From a3b2b4d4fd38c1c73a59b20ec1759ef7fc70c87c Mon Sep 17 00:00:00 2001 From: DaNike Date: Sat, 18 Oct 2025 21:28:01 -0500 Subject: [PATCH 01/15] Add support for shimming multiple versions of source packages --- src/GenApiCompatDll/GenApiCompatDll.csproj | 1 + src/GenApiCompatDll/Program.cs | 67 ++- src/MonoMod.Backports.Shims/ApiCompat.targets | 419 ++++++++--------- .../Directory.Build.targets | 424 ++++++++++-------- .../MonoMod.Backports.Shims.csproj | 5 + src/MonoMod.Backports/Directory.Build.props | 2 +- src/MonoMod.Backports/FilterTfms.targets | 144 +++--- .../MonoMod.Backports.csproj | 1 - src/ShimGen/Program.cs | 24 +- 9 files changed, 602 insertions(+), 485 deletions(-) diff --git a/src/GenApiCompatDll/GenApiCompatDll.csproj b/src/GenApiCompatDll/GenApiCompatDll.csproj index 7823bc7..0bf0557 100644 --- a/src/GenApiCompatDll/GenApiCompatDll.csproj +++ b/src/GenApiCompatDll/GenApiCompatDll.csproj @@ -11,6 +11,7 @@ + diff --git a/src/GenApiCompatDll/Program.cs b/src/GenApiCompatDll/Program.cs index ed8eb6a..28e0ea9 100644 --- a/src/GenApiCompatDll/Program.cs +++ b/src/GenApiCompatDll/Program.cs @@ -5,6 +5,7 @@ using AsmResolver.IO; using AsmResolver.PE.DotNet.Metadata.Tables; using NuGet.Frameworks; +using NuGet.Versioning; using System.Collections.Immutable; if (args is not [ @@ -42,12 +43,60 @@ .. var dotnetOobPackagePaths // load packages dict var packages = dotnetOobPackagePaths .Select(pkgPath - => (path: pkgPath, name: Path.GetFileName(Path.TrimEndingDirectorySeparator(Path.GetDirectoryName(Path.TrimEndingDirectorySeparator(pkgPath))!)), + => (path: pkgPath, + name: Path.GetFileName(Path.TrimEndingDirectorySeparator(Path.GetDirectoryName(Path.TrimEndingDirectorySeparator(pkgPath))!)), + version: new NuGet.Versioning.NuGetVersion((Path.GetFileName(Path.TrimEndingDirectorySeparator(pkgPath)))), fwks: Directory.EnumerateDirectories(Path.Combine(pkgPath, "lib")) .Select(libPath => (fwk: NuGetFramework.ParseFolder(Path.GetFileName(libPath)), files: Directory.GetFiles(libPath, "*.dll"))) .ToDictionary(t => t.fwk, t => t.files))) - .ToDictionary(t => t.name, t => (t.path, t.fwks)); + .GroupBy(t => t.name) + .Select(g => (name: g.Key, + fwks: g.Select(t => (t.fwks, t.version)) + .Aggregate(MergeFrameworks))) + .ToDictionary(t => t.name, t => t.fwks.fwks); + +(Dictionary d, NuGetVersion v) MergeFrameworks((Dictionary d, NuGetVersion v) a, (Dictionary d, NuGetVersion v) b) +{ + if (a.v == b.v) + { + // unify + var dict = new Dictionary>(); + foreach (var (k, v) in a.d) + { + dict.Add(k, v.ToHashSet()); + } + foreach (var (k, v) in b.d) + { + if (!dict.TryGetValue(k, out var l)) + { + dict.Add(k, l = new()); + } + foreach (var vi in v) + { + _ = l.Add(vi); + } + } + + return (dict.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToArray()), a.v); + } + + if (a.v > b.v) + { + // a is newer than b, add frameworks from b not present in a + foreach (var (k, v) in b.d) + { + _ = a.d.TryAdd(k, v); + } + + return (a.d, a.v); + } + else + { + // b is newer (or equal) to a, reverse parameters + return MergeFrameworks(b, a); + } +} // load available shims var allShimDlls = Directory.EnumerateFiles(shimsDir, "*.dll", new EnumerationOptions() { RecurseSubdirectories = true }) @@ -71,8 +120,11 @@ .. var dotnetOobPackagePaths // load our tfm list var packageTfms = reducer.ReduceEquivalent( packages - .SelectMany(t => t.Value.fwks) + .SelectMany(t => t.Value) .Select(t => t.Key) + .Where(f => f is not { Framework: ".NETStandard", Version.Major: < 2 }) // make sure we ignore netstandard1.x + .Where(f => DotNetRuntimeInfo.TryParse(f.GetDotNetFrameworkName(DefaultFrameworkNameProvider.Instance), out var rti) + && (rti.IsNetFramework || rti.IsNetStandard || rti.IsNetCoreApp)) ).ToArray(); var tfms = reducer.ReduceEquivalent( File.ReadAllLines(tfmsFilePath) @@ -89,7 +141,7 @@ .. var dotnetOobPackagePaths var exports = new Dictionary(); var assemblyRefsByName = new Dictionary(); var didReportError = false; -foreach (var (pkgName, (_, fwks)) in packages) +foreach (var (pkgName, fwks) in packages) { foreach (var file in fwks.SelectMany(kvp => kvp.Value)) { @@ -233,7 +285,7 @@ void ExportType(TypeExport export, IImplementation impl, bool nested) IImplementation impl; // resolve the implementation for this export - var pkgFwks = packages[export.FromPackage].fwks; + var pkgFwks = packages[export.FromPackage]; if (reducer.GetNearest(tfm, pkgFwks.Keys) is not null) { // valuetuple is a bit special... @@ -289,7 +341,7 @@ void ExportType(TypeExport export, IImplementation impl, bool nested) string GetReferencePathForTfm(NuGetFramework framework, bool useShim) { IEnumerable dlls = []; - foreach (var (_, (_, fwks)) in packages) + foreach (var (_, fwks) in packages) { var best = reducer.GetNearest(framework, fwks.Keys); if (best is null) continue; // no match, shouldn't be included @@ -298,7 +350,8 @@ string GetReferencePathForTfm(NuGetFramework framework, bool useShim) // if we want to use shims instead, remap all of the files to use the shims if (useShim) { - var shimFilesDict = shimsByTfm[reducer.GetNearest(best, shimsByTfm.Keys)!]; + var shimTfm = reducer.GetNearest(best, shimsByTfm.Keys) ?? reducer.GetNearest(framework, shimsByTfm.Keys); + var shimFilesDict = shimsByTfm[shimTfm!]; pkgFiles = pkgFiles .Select(f => Path.GetFileName(f)) .Select(n => shimFilesDict.TryGetValue(n, out var v) ? v : null) diff --git a/src/MonoMod.Backports.Shims/ApiCompat.targets b/src/MonoMod.Backports.Shims/ApiCompat.targets index 294c2e7..b11a133 100644 --- a/src/MonoMod.Backports.Shims/ApiCompat.targets +++ b/src/MonoMod.Backports.Shims/ApiCompat.targets @@ -1,210 +1,211 @@ - - - - - - - - - - <_DummyRestoreProjectDir>$(IntermediateOutputPath)dummy/ - <_DummyRestoreProjectPath>$(_DummyRestoreProjectDir)dummy.csproj - <_DummyRestoreProjectTemplate> - - - {{TFMS}} - false - false - false - false - - - - {{PKGREF}} - - - - - <_DummyPackageReferenceTemplate> - - - - <_ApiCompatAsmOut>$(IntermediateOutputPath)apicompat/ - <_BackportsTfmsTxt>$(IntermediateOutputPath)backports_tfms.txt - - - - - <_PkgPath>%(_ShimmedPackages.PkgPath) - - - <_PkgLibFolderPaths Remove="@(_PkgLibFolders)" /> - <_PkgLibFolderPaths Include="$([System.IO.Directory]::GetDirectories('$(_PkgPath)/lib/'))" /> - <_PkgLibFolders Remove="@(_PkgLibFolders)" /> - <_PkgLibFolders Include="@(_PkgLibFolderPaths->'$([System.IO.Path]::GetFileName('%(_PkgLibFolderPaths.Identity)'))')" /> - <_PkgCond Remove="@(_PkgCond)" /> - <_PkgCond Include="%(_PkgLibFolders.Identity)"> - $([MSBuild]::GetTargetFrameworkIdentifier('%(_PkgLibFolders.Identity)')) - $([MSBuild]::GetTargetFrameworkVersion('%(_PkgLibFolders.Identity)')) - - <_PkgCond> - - (%24([MSBuild]::GetTargetFrameworkIdentifier('%24(TargetFramework)')) == '%(TgtFwk)' - and %24([MSBuild]::VersionGreaterThanOrEquals(%24([MSBuild]::GetTargetFrameworkVersion('%24(TargetFramework)')),'%(TgtVer)'))) - - - - - <_PkgTfms>@(_PkgLibFolders) - <_PkgCond>@(_PkgCond->'%(Cond)',' or ') - - - <_ShimmedPackages Update="%(_ShimmedPackages.Identity)" Tfms="$(_PkgTfms)" TfmCond="$(_PkgCond)" /> - - - - - - - - - - - - - - - - <_BackportsTfms>@(_BackportsProps->'%(Value)') - - - <_BackportsTfms1 Include="$(_BackportsTfms)" /> - <_BackportsTfms1 Include="$(_TfmsWithFiles)" /> - <_BackportsTfms Include="@(_BackportsTfms1->Distinct())" /> - - - <_BackportsTfms>@(_BackportsTfms) - - - - - - - - - - - - - - - - - - - - - - <_NativeExecutableExtension Condition="'$(_NativeExecutableExtension)' == '' and '$(OS)' == 'Windows_NT'">.exe - <_GenApiCompatExe>%(MMGenApiCompat.RelativeDir)%(FileName)$(_NativeExecutableExtension) - - - - <_PPArguments Remove="@(_PPArguments)" /> - - <_PPArguments Include="$(IntermediateOutputPath)apicompat/" /> - - <_PPArguments Include="$(_BackportsTfmsTxt)" /> - - <_PPArguments Include="$(_ShimsDir)" /> - - <_PPArguments Include="%(_ShimmedPackages.PkgPath)" /> - - - - - - - - - - - - <_GenApiCompatParsed Include="%(_GenApiCompatOutput.Identity)"> - $([System.String]::Copy('%(Identity)').Split('|')[0]) - $([System.String]::Copy('%(Identity)').Split('|')[1]) - $([System.String]::Copy('%(Identity)').Split('|')[2]) - $([System.String]::Copy('%(Identity)').Split('|')[3]) - - - - - - - - <_Tfm>%(_GenApiCompatParsed.Tfm) - <_RefPath> - <_RefPath Condition="'$(_Tfm)' == '%(_ApiCompatRefPath.Identity)'">%(_ApiCompatRefPath.ReferencePath) - - - <_GenApiCompatParsed Update="%(_GenApiCompatParsed.Identity)"> - %(LeftRefPath),$(_RefPath) - %(RightRefPath),$(_RefPath) - - - - - - - - - - - - - <_ApiCompatValidateAssembliesSemaphoreFile>$(IntermediateOutputPath)$(MSBuildThisFileName).apicompat.semaphore - CollectApiCompatInputs;$(ApiCompatValidateAssembliesDependsOn);_ApiCompatFinalizeInputs;_WaitForBackportsBuild - - - - - - + + + + + + + + + + <_DummyRestoreProjectDir>$(IntermediateOutputPath)dummy/ + <_DummyRestoreProjectPath>$(_DummyRestoreProjectDir)dummy.csproj + <_DummyRestoreProjectTemplate> + + + {{TFMS}} + false + false + false + false + + + + {{PKGREF}} + + + + + <_DummyPackageReferenceTemplate> + + + + <_ApiCompatAsmOut>$(IntermediateOutputPath)apicompat/ + <_BackportsTfmsTxt>$(IntermediateOutputPath)backports_tfms.txt + + + + + + <_PkgPath>%(_ShimmedPackages.PkgPath) + + + <_PkgLibFolderPaths Remove="@(_PkgLibFolders)" /> + <_PkgLibFolderPaths Include="$([System.IO.Directory]::GetDirectories('$(_PkgPath)/lib/'))" /> + <_PkgLibFolders Remove="@(_PkgLibFolders)" /> + <_PkgLibFolders Include="@(_PkgLibFolderPaths->'$([System.IO.Path]::GetFileName('%(_PkgLibFolderPaths.Identity)'))')" /> + <_PkgCond Remove="@(_PkgCond)" /> + <_PkgCond Include="%(_PkgLibFolders.Identity)"> + $([MSBuild]::GetTargetFrameworkIdentifier('%(_PkgLibFolders.Identity)')) + $([MSBuild]::GetTargetFrameworkVersion('%(_PkgLibFolders.Identity)')) + + <_PkgCond> + + (%24([MSBuild]::GetTargetFrameworkIdentifier('%24(TargetFramework)')) == '%(TgtFwk)' + and %24([MSBuild]::VersionGreaterThanOrEquals(%24([MSBuild]::GetTargetFrameworkVersion('%24(TargetFramework)')),'%(TgtVer)'))) + + + + + <_PkgTfms>@(_PkgLibFolders) + <_PkgCond>@(_PkgCond->'%(Cond)',' or ') + + + <_ShimmedPackages Update="%(_ShimmedPackages.Identity)" Tfms="$(_PkgTfms)" TfmCond="$(_PkgCond)" /> + + + + + + + + + + + + + + + + <_BackportsTfms>@(_BackportsProps->'%(Value)') + + + <_BackportsTfms1 Include="$(_BackportsTfms)" /> + <_BackportsTfms1 Include="$(_TfmsWithFiles)" /> + <_BackportsTfms Include="@(_BackportsTfms1->Distinct())" /> + + + <_BackportsTfms>@(_BackportsTfms) + + + + + + + + + + + + + + + + + + + + + + <_NativeExecutableExtension Condition="'$(_NativeExecutableExtension)' == '' and '$(OS)' == 'Windows_NT'">.exe + <_GenApiCompatExe>%(MMGenApiCompat.RelativeDir)%(FileName)$(_NativeExecutableExtension) + + + + <_PPArguments Remove="@(_PPArguments)" /> + + <_PPArguments Include="$(IntermediateOutputPath)apicompat/" /> + + <_PPArguments Include="$(_BackportsTfmsTxt)" /> + + <_PPArguments Include="$(_ShimsDir)" /> + + <_PPArguments Include="@(_ShimmedPackagePaths)" /> + + + + + + + + + + + + <_GenApiCompatParsed Include="%(_GenApiCompatOutput.Identity)"> + $([System.String]::Copy('%(Identity)').Split('|')[0]) + $([System.String]::Copy('%(Identity)').Split('|')[1]) + $([System.String]::Copy('%(Identity)').Split('|')[2]) + $([System.String]::Copy('%(Identity)').Split('|')[3]) + + + + + + + + <_Tfm>%(_GenApiCompatParsed.Tfm) + <_RefPath> + <_RefPath Condition="'$(_Tfm)' == '%(_ApiCompatRefPath.Identity)'">%(_ApiCompatRefPath.ReferencePath) + + + <_GenApiCompatParsed Update="%(_GenApiCompatParsed.Identity)"> + %(LeftRefPath),$(_RefPath) + %(RightRefPath),$(_RefPath) + + + + + + + + + + + + + <_ApiCompatValidateAssembliesSemaphoreFile>$(IntermediateOutputPath)$(MSBuildThisFileName).apicompat.semaphore + CollectApiCompatInputs;$(ApiCompatValidateAssembliesDependsOn);_ApiCompatFinalizeInputs;_WaitForBackportsBuild + + + + + + \ No newline at end of file diff --git a/src/MonoMod.Backports.Shims/Directory.Build.targets b/src/MonoMod.Backports.Shims/Directory.Build.targets index 11e3073..8e750f3 100644 --- a/src/MonoMod.Backports.Shims/Directory.Build.targets +++ b/src/MonoMod.Backports.Shims/Directory.Build.targets @@ -1,192 +1,234 @@ - - - - - - - - - - - - false - - - - - <_ShimsDir>$(IntermediateOutputPath)shims/ - <_OutputTfmsTxt>$(IntermediateOutputPath)tfms.txt - - - - - - none - false - true - - - - - - - <_ShimmedPackages Include="@(PackageReference)" Condition="'%(Shim)' == 'true'"> - Pkg$([System.String]::Copy('%(Identity)').Replace('.','_')) - - - - <_ShimmedPackages> - $(%(PkgProp)) - - - - - - - - <_ExistingShimFiles Include="$(_ShimsDir)**/*" /> - - - - - - - <_NativeExecutableExtension Condition="'$(_NativeExecutableExtension)' == '' and '$(OS)' == 'Windows_NT'">.exe - <_ShimGenExe>%(MMShimGen.RelativeDir)%(FileName)$(_NativeExecutableExtension) - <_SnkDir>$(MMRootPath)snk/ - - - - <_PPArguments Remove="@(_PPArguments)" /> - <_PPArguments Include="$(_ShimsDir)" /> - <_PPArguments Include="$(_SnkDir)" /> - - <_PPArguments Include="%(_ShimmedPackages.PkgPath)" /> - - - - - - - - - <_ShimFiles Include="$(_ShimsDir)**/*.dll" /> - - - - - <_DebugSymbolsIntermediatePath Remove="@(_DebugSymbolsIntermediatePath)" /> - <_ShimFiles Include="$(_ShimsDir)**/*.pdb" /> - - <_ShimFiles Include="$(_ShimsDir)*.xml" /> - - - - - - - <_TfmDirs Include="$([System.String]::Copy('%(_ShimGenOutput.Identity)').Replace('tfm:', ''))" - Condition="$([System.String]::Copy('%(_ShimGenOutput.Identity)').StartsWith('tfm:'))" /> - - - - - - - - - - - - - - - - - - - - - <_PackageMinTfms Include="$(PackageMinTfms)" /> - - - <_TfmsWithFiles>@(PackageFile->'%(TargetFramework)') - - - <_PackageMinTfms Include="@(PackageFile->'%(TargetFramework)')" /> - - - - - - - - - - - - - - <_OverridePackages>@(_ShimmedPackages->'%(Identity)|%(Version)') - <_ImportedPropOpen>]]> - <_ImportedPropClose>]]> - <_BuildFileContent> - - - $(_ImportedPropOpen)true$(_ImportedPropClose) - - - - $(_OverridePackages) - - - - - <_BuildTransitiveContent> - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + false + + + + + + + <_PkgName>%(ExtraShimPackageVersion.Identity) + <_VerList>%(ExtraShimPackageVersion.Version) + + + <_VerList Remove="@(_VerList)" /> + <_VerList Include="$(_VerList)" /> + + + + @(_VerList->'[%(Identity)]',';') + + + + + + <_ShimsDir>$(IntermediateOutputPath)shims/ + <_OutputTfmsTxt>$(IntermediateOutputPath)tfms.txt + + + + + + none + false + true + + + + + + + <_PkgName>%(ExtraShimPackageVersion.Identity) + <_VerList>%(ExtraShimPackageVersion.Version) + <_PkgNameLower>$([System.String]::Copy($(_PkgName)).ToLowerInvariant()) + <_PkgBasePath>$([MSBuild]::NormalizePath('$(NuGetPackageRoot)', '$(_PkgNameLower)')) + + + <_VerList Remove="@(_VerList)" /> + <_VerList Include="$(_VerList)" /> + + + <_VerList> + $([MSBuild]::NormalizePath('$(_PkgBasePath)', '%(_VerList.Identity)')) + + <_ShimmedPackagePaths Include="@(_VerList->'%(Path)')"/> + + + + + + <_ShimmedPackages Include="@(PackageReference)" Condition="'%(Shim)' == 'true'"> + Pkg$([System.String]::Copy('%(Identity)').Replace('.','_')) + + + + <_ShimmedPackages> + $(%(PkgProp)) + + + + + <_ShimmedPackagePaths Include="@(_ShimmedPackages->'%(PkgPath)')" /> + + + + + + + <_ExistingShimFiles Include="$(_ShimsDir)**/*" /> + + + + + + + <_NativeExecutableExtension Condition="'$(_NativeExecutableExtension)' == '' and '$(OS)' == 'Windows_NT'">.exe + <_ShimGenExe>%(MMShimGen.RelativeDir)%(FileName)$(_NativeExecutableExtension) + <_SnkDir>$(MMRootPath)snk/ + + + + <_PPArguments Remove="@(_PPArguments)" /> + <_PPArguments Include="$(_ShimsDir)" /> + <_PPArguments Include="$(_SnkDir)" /> + + <_PPArguments Include="@(_ShimmedPackagePaths)" /> + + + + + + + + + <_ShimFiles Include="$(_ShimsDir)**/*.dll" /> + + + + + <_DebugSymbolsIntermediatePath Remove="@(_DebugSymbolsIntermediatePath)" /> + <_ShimFiles Include="$(_ShimsDir)**/*.pdb" /> + + <_ShimFiles Include="$(_ShimsDir)*.xml" /> + + + + + + + <_TfmDirs Include="$([System.String]::Copy('%(_ShimGenOutput.Identity)').Replace('tfm:', ''))" + Condition="$([System.String]::Copy('%(_ShimGenOutput.Identity)').StartsWith('tfm:'))" /> + + + + + + + + + + + + + + + + + + + + + <_PackageMinTfms Include="$(PackageMinTfms)" /> + + + <_TfmsWithFiles>@(PackageFile->'%(TargetFramework)') + + + <_PackageMinTfms Include="@(PackageFile->'%(TargetFramework)')" /> + + + + + + + + + + + + + + <_OverridePackages>@(_ShimmedPackages->'%(Identity)|%(Version)') + <_ImportedPropOpen>]]> + <_ImportedPropClose>]]> + <_BuildFileContent> + + + $(_ImportedPropOpen)true$(_ImportedPropClose) + + + + $(_OverridePackages) + + + + + <_BuildTransitiveContent> + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/MonoMod.Backports.Shims/MonoMod.Backports.Shims.csproj b/src/MonoMod.Backports.Shims/MonoMod.Backports.Shims.csproj index c52ed54..cd284e1 100644 --- a/src/MonoMod.Backports.Shims/MonoMod.Backports.Shims.csproj +++ b/src/MonoMod.Backports.Shims/MonoMod.Backports.Shims.csproj @@ -26,6 +26,11 @@ + + + + + diff --git a/src/MonoMod.Backports/Directory.Build.props b/src/MonoMod.Backports/Directory.Build.props index af38c79..801e033 100644 --- a/src/MonoMod.Backports/Directory.Build.props +++ b/src/MonoMod.Backports/Directory.Build.props @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/src/MonoMod.Backports/FilterTfms.targets b/src/MonoMod.Backports/FilterTfms.targets index 436b4b5..f8eff0f 100644 --- a/src/MonoMod.Backports/FilterTfms.targets +++ b/src/MonoMod.Backports/FilterTfms.targets @@ -1,73 +1,73 @@ - - - - - true - - - - - - - - - $([MSBuild]::NormalizePath('$(NuGetPackageRoot)', 'system.collections.immutable', '6.0.0')) - - - - - - - - - - - - - - - - - - <_MSBPropsFile>$(IntermediateOutputPath)Backports.TfmFilter.props - - - - - - - - <_CompileUnfiltered Include="@(Compile)" /> - <_CompileUnfiltered Include="@($(CompileRemovedItem))" Condition="'$(CompileRemovedItem)' != ''" /> - - - - - - - - - - - - - <_CompileUnfiltered Include="@(Compile)" /> - - - - - - - - - - + + + + + true + + + + + + + + + $([MSBuild]::NormalizePath('$(NuGetPackageRoot)', 'system.collections.immutable', '6.0.0')) + + + + + + + + + + + + + + + + + + <_MSBPropsFile>$(IntermediateOutputPath)Backports.TfmFilter.props + + + + + + + + <_CompileUnfiltered Include="@(Compile)" /> + <_CompileUnfiltered Include="@($(CompileRemovedItem))" Condition="'$(CompileRemovedItem)' != ''" /> + + + + + + + + + + + + + <_CompileUnfiltered Include="@(Compile)" /> + + + + + + + + + + \ No newline at end of file diff --git a/src/MonoMod.Backports/MonoMod.Backports.csproj b/src/MonoMod.Backports/MonoMod.Backports.csproj index 7c31ee3..5450345 100644 --- a/src/MonoMod.Backports/MonoMod.Backports.csproj +++ b/src/MonoMod.Backports/MonoMod.Backports.csproj @@ -33,4 +33,3 @@ - diff --git a/src/ShimGen/Program.cs b/src/ShimGen/Program.cs index 4aeb5f8..51f3ef8 100644 --- a/src/ShimGen/Program.cs +++ b/src/ShimGen/Program.cs @@ -61,12 +61,22 @@ .. var dotnetOobPackagePaths // first, arrange the lookups in a reasonable manner // pkgPath -> framework -> dllPaths var packageLayout = new Dictionary>>(); -var dllsByDllName = new Dictionary>(); +var dllsByDllName = new Dictionary>>(); foreach (var (pkgPath, libPath, framework, dllPath) in pkgList .SelectMany(ta => ta.Item2.SelectMany(tb => tb.dlls.Select(dll => (ta.pkgPath, tb.libPath, tb.fwk, dll))))) { + /*if (framework is + { + Framework: ".NETStandard", + Version.Major: < 2 + }) + { + // this is .NET Standard < 2.0; we do not want to try to support it. Skip. + continue; + }*/ + if (!packageLayout.TryGetValue(pkgPath, out var fwDict)) { packageLayout.Add(pkgPath, fwDict = new()); @@ -90,13 +100,19 @@ .. var dotnetOobPackagePaths dllsByDllName.Add(dllName, dllPathList = new()); } - dllPathList.Add(framework, dllPath); + if (!dllPathList.TryGetValue(framework, out var dllPathSet)) + { + dllPathList.Add(framework, dllPathSet = new()); + } + + dllPathSet.Add(dllPath); } // collect the list of ALL target frameworks that we might care about var targetTfms = fwReducer .ReduceEquivalent(packageLayout.Values.SelectMany(v => v.Keys)) .Where(fwk => fwk.Framework is ".NETFramework" or ".NETStandard" or ".NETCoreApp") // filter to just the standard Frameworks, because AsmResolver can't handle all the wacko ones + .Where(fwk => fwk is not { Framework: ".NETStandard", Version.Major: < 2 }) .ToArray(); // then build up a mapping of the source files for all of those TFMs @@ -153,7 +169,7 @@ .. var dotnetOobPackagePaths AssemblyDefinition? backportsShimAssembly = null; AssemblyReference? backportsReference = null; - var bclShimPath = GetFrameworkKey(dllsByDllName[dllName], targetTfm, fwReducer); + var bclShimPath = GetFrameworkKey(dllsByDllName[dllName], targetTfm, fwReducer).First(); var bclShim = ModuleDefinition.FromFile(bclShimPath, readerParams); @@ -179,7 +195,7 @@ .. var dotnetOobPackagePaths new AssemblyReference("MonoMod.Backports", new(1, 0, 0, 0)) .ImportWith(backportsShim.DefaultImporter); - foreach (var file in dllsByDllName[dllName].Values) + foreach (var file in dllsByDllName[dllName].Values.SelectMany(x => x)) { bclShim = ModuleDefinition.FromFile(file, readerParams); From 8e8eb765fb3598b0c2235c20221181a576c972d0 Mon Sep 17 00:00:00 2001 From: DaNike Date: Sat, 18 Oct 2025 21:30:17 -0500 Subject: [PATCH 02/15] In the conflict overrides, always use an extremely large versions --- src/MonoMod.Backports.Shims/Directory.Build.targets | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/MonoMod.Backports.Shims/Directory.Build.targets b/src/MonoMod.Backports.Shims/Directory.Build.targets index 8e750f3..f94a2d8 100644 --- a/src/MonoMod.Backports.Shims/Directory.Build.targets +++ b/src/MonoMod.Backports.Shims/Directory.Build.targets @@ -193,7 +193,9 @@ - <_OverridePackages>@(_ShimmedPackages->'%(Identity)|%(Version)') + + + <_OverridePackages>@(_ShimmedPackages->'%(Identity)|9999.9999.9999.9999') <_ImportedPropOpen>]]> <_ImportedPropClose>]]> <_BuildFileContent> From 3e0994befe58457a4de12443b5e15811c5ab480b Mon Sep 17 00:00:00 2001 From: DaNike Date: Sun, 19 Oct 2025 23:55:15 -0500 Subject: [PATCH 03/15] Ensure we restore the dummy project properly to get all dependencies --- MonoMod.Backports.slnx | 1 + .../FilterPackagesForRestore.csproj | 16 ++++ src/FilterPackagesForRestore/Program.cs | 75 +++++++++++++++++++ src/GenApiCompatDll/Program.cs | 29 +++++-- src/MonoMod.Backports.Shims/ApiCompat.targets | 45 +++++++++-- .../Directory.Build.targets | 42 ++++++++++- src/ShimGen/Program.cs | 30 ++++---- 7 files changed, 207 insertions(+), 31 deletions(-) create mode 100644 src/FilterPackagesForRestore/FilterPackagesForRestore.csproj create mode 100644 src/FilterPackagesForRestore/Program.cs diff --git a/MonoMod.Backports.slnx b/MonoMod.Backports.slnx index 3841476..e82f115 100644 --- a/MonoMod.Backports.slnx +++ b/MonoMod.Backports.slnx @@ -18,6 +18,7 @@ + diff --git a/src/FilterPackagesForRestore/FilterPackagesForRestore.csproj b/src/FilterPackagesForRestore/FilterPackagesForRestore.csproj new file mode 100644 index 0000000..248f4ac --- /dev/null +++ b/src/FilterPackagesForRestore/FilterPackagesForRestore.csproj @@ -0,0 +1,16 @@ + + + + Exe + net9.0 + enable + enable + false + + + + + + + + \ No newline at end of file diff --git a/src/FilterPackagesForRestore/Program.cs b/src/FilterPackagesForRestore/Program.cs new file mode 100644 index 0000000..53ef061 --- /dev/null +++ b/src/FilterPackagesForRestore/Program.cs @@ -0,0 +1,75 @@ +using NuGet.Frameworks; +using NuGet.Versioning; + +if (args is not [ + var tfmsFilePath, + .. var dotnetOobPackagePaths + ]) +{ + Console.Error.WriteLine("Assemblies not provided."); + Console.Error.WriteLine("Syntax: <...oob package paths...>"); + Console.Error.WriteLine("Arguments provided: "); + foreach (var arg in args) + { + Console.Error.WriteLine($"- {arg}"); + } + return 1; +} + +var reducer = new FrameworkReducer(); +var precSorter = new FrameworkPrecedenceSorter(DefaultFrameworkNameProvider.Instance, false); + +// load packages dict +var packages = dotnetOobPackagePaths + .Select(pkgPath + => (name: Path.GetFileName(Path.TrimEndingDirectorySeparator(Path.GetDirectoryName(Path.TrimEndingDirectorySeparator(pkgPath))!)), + version: new NuGetVersion((Path.GetFileName(Path.TrimEndingDirectorySeparator(pkgPath)))), + fwks: Directory.EnumerateDirectories(Path.Combine(pkgPath, "lib")) + .Select(libPath => NuGetFramework.ParseFolder(Path.GetFileName(libPath))) + .ToArray())) + .GroupBy(t => t.name) + .Select(g + => (name: g.Key, + fwksForVer: g + .Select(t => (t.version, t.fwks)) + .OrderByDescending(t => t.version) + .ToArray())); + +var tfms = reducer.ReduceEquivalent( + File.ReadAllLines(tfmsFilePath) + .Select(NuGetFramework.ParseFolder) + ) + .Order(precSorter) + .ToArray(); + +foreach (var tfm in tfms) +{ + Console.Write($""); + + foreach (var (pkgName, fwkByVer) in packages) + { + NuGetVersion? resolvedVer = null; + foreach (var (ver, fwks) in fwkByVer) + { + if (resolvedVer is not null && resolvedVer > ver) + { + continue; + } + + if (reducer.GetNearest(tfm, fwks) is not null) + { + resolvedVer = ver; + } + } + + // no matching version is actually ok, it's fine, we just don't want to output anything for it + if (resolvedVer is not null) + { + Console.Write($""); + } + } + + Console.WriteLine(""); +} + +return 0; \ No newline at end of file diff --git a/src/GenApiCompatDll/Program.cs b/src/GenApiCompatDll/Program.cs index 28e0ea9..2d26808 100644 --- a/src/GenApiCompatDll/Program.cs +++ b/src/GenApiCompatDll/Program.cs @@ -118,19 +118,32 @@ .. var dotnetOobPackagePaths // load our tfm list -var packageTfms = reducer.ReduceEquivalent( +var packageTfmsRaw = reducer.ReduceEquivalent( packages .SelectMany(t => t.Value) .Select(t => t.Key) + ).ToArray(); + +var packageTfmsDirect = + packageTfmsRaw .Where(f => f is not { Framework: ".NETStandard", Version.Major: < 2 }) // make sure we ignore netstandard1.x .Where(f => DotNetRuntimeInfo.TryParse(f.GetDotNetFrameworkName(DefaultFrameworkNameProvider.Instance), out var rti) - && (rti.IsNetFramework || rti.IsNetStandard || rti.IsNetCoreApp)) - ).ToArray(); -var tfms = reducer.ReduceEquivalent( + && (rti.IsNetFramework || rti.IsNetStandard || rti.IsNetCoreApp)); + +var backportsTfms = reducer.ReduceEquivalent( File.ReadAllLines(tfmsFilePath) - .Select(NuGetFramework.ParseFolder) - .Concat(packageTfms) - ) + .Select(NuGetFramework.ParseFolder) + ).ToArray(); +var packageTfmsIndirect = backportsTfms + .Where(tfm + => packages.Any(kvp => reducer.GetNearest(tfm, kvp.Value.Keys) is not null)); + +var packageTfms = reducer.ReduceEquivalent( + packageTfmsDirect.Concat(packageTfmsIndirect) + ).ToArray(); + +var tfms = reducer + .ReduceEquivalent(backportsTfms.Concat(packageTfmsDirect)) .Order(precSorter) .ToArray(); @@ -350,7 +363,7 @@ string GetReferencePathForTfm(NuGetFramework framework, bool useShim) // if we want to use shims instead, remap all of the files to use the shims if (useShim) { - var shimTfm = reducer.GetNearest(best, shimsByTfm.Keys) ?? reducer.GetNearest(framework, shimsByTfm.Keys); + var shimTfm = reducer.GetNearest(framework, shimsByTfm.Keys); var shimFilesDict = shimsByTfm[shimTfm!]; pkgFiles = pkgFiles .Select(f => Path.GetFileName(f)) diff --git a/src/MonoMod.Backports.Shims/ApiCompat.targets b/src/MonoMod.Backports.Shims/ApiCompat.targets index b11a133..74cd5f2 100644 --- a/src/MonoMod.Backports.Shims/ApiCompat.targets +++ b/src/MonoMod.Backports.Shims/ApiCompat.targets @@ -9,6 +9,12 @@ Private="false" Pack="false" SetTargetFramework="TargetFramework=net9.0" SkipGetTargetFrameworkProperties="true" /> + @@ -22,11 +28,12 @@ false false false + true - {{PKGREF}} + {{PKGREF}} @@ -35,7 +42,7 @@ <_ApiCompatAsmOut>$(IntermediateOutputPath)apicompat/ - <_BackportsTfmsTxt>$(IntermediateOutputPath)backports_tfms.txt + <_BackportsTfmsTxt2>$(IntermediateOutputPath)backports_tfms_final.txt @@ -74,6 +81,9 @@ + + <_BackportsProps Remove="@(_BackportsProps)"/> + @(_BackportsProps->'%(Value)') + <_BackportsTfms1 Remove="@(_BackportsTfms1)" /> <_BackportsTfms1 Include="$(_BackportsTfms)" /> <_BackportsTfms1 Include="$(_TfmsWithFiles)" /> + <_BackportsTfms Remove="@(_BackportsTfms)" /> <_BackportsTfms Include="@(_BackportsTfms1->Distinct())" /> <_BackportsTfms>@(_BackportsTfms) - + + + + <_NativeExecutableExtension Condition="'$(_NativeExecutableExtension)' == '' and '$(OS)' == 'Windows_NT'">.exe + <_FilterPkgsExe>%(MMFilterPkgs.RelativeDir)%(FileName)$(_NativeExecutableExtension) + + + + <_PPArguments Remove="@(_PPArguments)" /> + + <_PPArguments Include="$(_BackportsTfmsTxt2)" /> + + <_PPArguments Include="@(_ShimmedPackagePaths)" /> + + + + + + + + <_PkgRefs>@(_FilterPkgsOutput->'%(Identity)','%0a') + - + @@ -116,7 +150,6 @@ - <_NativeExecutableExtension Condition="'$(_NativeExecutableExtension)' == '' and '$(OS)' == 'Windows_NT'">.exe <_GenApiCompatExe>%(MMGenApiCompat.RelativeDir)%(FileName)$(_NativeExecutableExtension) @@ -125,7 +158,7 @@ <_PPArguments Include="$(IntermediateOutputPath)apicompat/" /> - <_PPArguments Include="$(_BackportsTfmsTxt)" /> + <_PPArguments Include="$(_BackportsTfmsTxt2)" /> <_PPArguments Include="$(_ShimsDir)" /> diff --git a/src/MonoMod.Backports.Shims/Directory.Build.targets b/src/MonoMod.Backports.Shims/Directory.Build.targets index f94a2d8..88c58c9 100644 --- a/src/MonoMod.Backports.Shims/Directory.Build.targets +++ b/src/MonoMod.Backports.Shims/Directory.Build.targets @@ -40,6 +40,7 @@ <_ShimsDir>$(IntermediateOutputPath)shims/ <_OutputTfmsTxt>$(IntermediateOutputPath)tfms.txt + <_BackportsTfmsTxt>$(IntermediateOutputPath)backports_tfms.txt + + + <_BackportsProps Remove="@(_BackportsProps)"/> + + + + + + + + + <_BackportsTfms>@(_BackportsProps->'%(Value)') + + + <_BackportsTfms1 Remove="@(_BackportsTfms1)" /> + <_BackportsTfms1 Include="$(_BackportsTfms)" /> + <_BackportsTfms1 Include="$(PackageMinTfms)" /> + <_BackportsTfms Remove="@(_BackportsTfms)" /> + <_BackportsTfms Include="@(_BackportsTfms1->Distinct())" /> + + + <_BackportsTfms>@(_BackportsTfms) + + + + + <_PPArguments Remove="@(_PPArguments)" /> - <_PPArguments Include="$(_ShimsDir)" /> - <_PPArguments Include="$(_SnkDir)" /> - + + <_PPArguments Include="$(_ShimsDir)" /> + + <_PPArguments Include="$(_SnkDir)" /> + + <_PPArguments Include="$(_BackportsTfmsTxt)" /> + <_PPArguments Include="@(_ShimmedPackagePaths)" /> diff --git a/src/ShimGen/Program.cs b/src/ShimGen/Program.cs index 51f3ef8..bf99141 100644 --- a/src/ShimGen/Program.cs +++ b/src/ShimGen/Program.cs @@ -15,11 +15,12 @@ if (args is not [ var outputRefDir, var snkPath, + var tfmsFilePath, .. var dotnetOobPackagePaths ]) { Console.Error.WriteLine("Assemblies not provided."); - Console.Error.WriteLine("Syntax: <...oob package paths...>"); + Console.Error.WriteLine("Syntax: <...oob package paths...>"); Console.Error.WriteLine("Arguments provided: "); foreach (var arg in args) { @@ -67,16 +68,6 @@ .. var dotnetOobPackagePaths => ta.Item2.SelectMany(tb => tb.dlls.Select(dll => (ta.pkgPath, tb.libPath, tb.fwk, dll))))) { - /*if (framework is - { - Framework: ".NETStandard", - Version.Major: < 2 - }) - { - // this is .NET Standard < 2.0; we do not want to try to support it. Skip. - continue; - }*/ - if (!packageLayout.TryGetValue(pkgPath, out var fwDict)) { packageLayout.Add(pkgPath, fwDict = new()); @@ -109,12 +100,25 @@ .. var dotnetOobPackagePaths } // collect the list of ALL target frameworks that we might care about -var targetTfms = fwReducer + +var packageTfmsDirect = fwReducer .ReduceEquivalent(packageLayout.Values.SelectMany(v => v.Keys)) .Where(fwk => fwk.Framework is ".NETFramework" or ".NETStandard" or ".NETCoreApp") // filter to just the standard Frameworks, because AsmResolver can't handle all the wacko ones .Where(fwk => fwk is not { Framework: ".NETStandard", Version.Major: < 2 }) .ToArray(); +var backportsTfms = fwReducer.ReduceEquivalent( + File.ReadAllLines(tfmsFilePath) + .Select(NuGetFramework.ParseFolder) + ).ToArray(); +var packageTfmsIndirect = backportsTfms + .Where(tfm + => packageLayout.Any(kvp => fwReducer.GetNearest(tfm, kvp.Value.Keys) is not null)); + +var targetTfms = fwReducer.ReduceEquivalent( + packageTfmsDirect.Concat(packageTfmsIndirect) + ).ToArray(); + // then build up a mapping of the source files for all of those TFMs var frameworkGroupLayout = new Dictionary>(); foreach (var tfm in targetTfms) @@ -143,7 +147,7 @@ .. var dotnetOobPackagePaths var precSorter = new FrameworkPrecedenceSorter(DefaultFrameworkNameProvider.Instance, false); // now we group by unique sets, and pick only the minimial framework for each (of each type) -// this is necesasry because our final package will eventually have a dummy reference for the minimum supported +// this is necessary because our final package will eventually have a dummy reference for the minimum supported // for each (particularly net35), but if we just pick the overall minimum (netstandard2.0), net35 would be preferred // for all .NET Framework targets, even the ones that support NS2.0. var frameworkAssemblies = frameworkGroupLayout From a01bf1da2b9a7ff4eb254df346b1a3d02d1aecc7 Mon Sep 17 00:00:00 2001 From: DaNike Date: Sun, 19 Oct 2025 23:57:01 -0500 Subject: [PATCH 04/15] Make sure dummy restore gets reference assemblies --- src/MonoMod.Backports.Shims/ApiCompat.targets | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/MonoMod.Backports.Shims/ApiCompat.targets b/src/MonoMod.Backports.Shims/ApiCompat.targets index 74cd5f2..26d296e 100644 --- a/src/MonoMod.Backports.Shims/ApiCompat.targets +++ b/src/MonoMod.Backports.Shims/ApiCompat.targets @@ -30,6 +30,13 @@ false true + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + From 993263b5fd72fc58ec4eda5d00d65ffdde514b02 Mon Sep 17 00:00:00 2001 From: DaNike Date: Mon, 20 Oct 2025 00:00:03 -0500 Subject: [PATCH 05/15] Suppress Unsafe.Unbox struct constraint compat error --- .../CompatibilitySuppressions.xml | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/MonoMod.Backports.Shims/CompatibilitySuppressions.xml diff --git a/src/MonoMod.Backports.Shims/CompatibilitySuppressions.xml b/src/MonoMod.Backports.Shims/CompatibilitySuppressions.xml new file mode 100644 index 0000000..bcc2e08 --- /dev/null +++ b/src/MonoMod.Backports.Shims/CompatibilitySuppressions.xml @@ -0,0 +1,52 @@ + + + + + CP0021 + M:System.Runtime.CompilerServices.Unsafe.Unbox``1(System.Object)``0:struct + net45 baseline + net45 shimmed + + + CP0021 + M:System.Runtime.CompilerServices.Unsafe.Unbox``1(System.Object)``0:struct + net452 baseline + net452 shimmed + + + CP0021 + M:System.Runtime.CompilerServices.Unsafe.Unbox``1(System.Object)``0:struct + net461 baseline + net461 shimmed + + + CP0021 + M:System.Runtime.CompilerServices.Unsafe.Unbox``1(System.Object)``0:struct + net5.0 baseline + net5.0 shimmed + + + CP0021 + M:System.Runtime.CompilerServices.Unsafe.Unbox``1(System.Object)``0:struct + netcoreapp2.0 baseline + netcoreapp2.0 shimmed + + + CP0021 + M:System.Runtime.CompilerServices.Unsafe.Unbox``1(System.Object)``0:struct + netcoreapp2.1 baseline + netcoreapp2.1 shimmed + + + CP0021 + M:System.Runtime.CompilerServices.Unsafe.Unbox``1(System.Object)``0:struct + netcoreapp3.0 baseline + netcoreapp3.0 shimmed + + + CP0021 + M:System.Runtime.CompilerServices.Unsafe.Unbox``1(System.Object)``0:struct + netcoreapp3.1 baseline + netcoreapp3.1 shimmed + + \ No newline at end of file From 5b7972832fb04f06a81f54c07438459dba7241bf Mon Sep 17 00:00:00 2001 From: js6pak Date: Tue, 20 May 2025 16:27:12 +0200 Subject: [PATCH 06/15] wip --- .github/workflows/main.yml | 63 ++ .gitignore | 23 + .../CompatUnbreaker.Attributes.csproj | 11 + .../NotSupportedAnymoreException.cs | 16 + .../UnbreakerConstructorAttribute.cs | 4 + .../UnbreakerExtensionAttribute.cs | 9 + .../UnbreakerExtensionsAttribute.cs | 4 + .../UnbreakerFieldAttribute.cs | 4 + .../UnbreakerRenameAttribute.cs | 45 ++ .../UnbreakerReplaceAttribute.cs | 9 + .../UnbreakerShimAttribute.cs | 9 + .../UnbreakerThisAttribute.cs | 7 + CompatUnbreaker.Attributes/packages.lock.json | 117 ++++ .../AsmResolverSyntaxGeneratorTests.cs | 172 +++++ .../CompatUnbreaker.Tests.csproj | 21 + .../TestFiles/AttributeTests.cs | 80 +++ .../TestFiles/DllImportTests.cs | 12 + .../TestFiles/MethodTests.cs | 26 + .../TestFiles/NamedTupleTests.cs | 14 + .../TestFiles/ReservedTests.cs | 28 + CompatUnbreaker.Tests/packages.lock.json | 567 ++++++++++++++++ .../AssemblyMapping/AssemblyMapper.cs | 64 ++ .../AssemblyMapping/ElementMapper.cs | 40 ++ .../AssemblyMapping/ElementSide.cs | 7 + .../AssemblyMapping/MapperSettings.cs | 16 + .../AssemblyMapping/MemberMapper.cs | 8 + .../AssemblyMapping/TypeMapper.cs | 58 ++ .../ApiCompatibility/Comparing/ApiComparer.cs | 81 +++ .../Comparing/CompatDifference.cs | 32 + .../Comparing/DifferenceType.cs | 8 + .../Comparing/Rules/BaseRule.cs | 18 + .../Rules/CannotAddAbstractMember.cs | 31 + .../Rules/CannotAddMemberToInterface.cs | 54 ++ .../Rules/CannotAddOrRemoveVirtualKeyword.cs | 61 ++ .../Rules/CannotChangeGenericConstraints.cs | 130 ++++ .../Comparing/Rules/CannotChangeVisibility.cs | 79 +++ .../Rules/CannotRemoveBaseTypeOrInterface.cs | 92 +++ .../Comparing/Rules/CannotSealType.cs | 33 + .../Comparing/Rules/EnumsMustMatch.cs | 76 +++ .../Comparing/Rules/MembersMustExist.cs | 91 +++ .../ApiCompatibility/README.md | 1 + .../Commands/BaseShimProjectCommand.cs | 112 ++++ .../Commands/CompareCommand.cs | 21 + .../Commands/SkeletonCommand.cs | 15 + .../CompatUnbreaker.Tool.csproj | 35 + CompatUnbreaker.Tool/Program.cs | 17 + .../AsmResolverTypeSyntaxGenerator.cs | 377 +++++++++++ .../DeclarationModifiersExtensions.cs | 35 + .../CodeGeneration/NullableAnnotation.cs | 8 + .../RoslynIdentifierExtensions.cs | 57 ++ .../SyntaxGeneratorExtensions.Attributes.cs | 184 +++++ .../SyntaxGeneratorExtensions.EnumValues.cs | 216 ++++++ .../SyntaxGeneratorExtensions.Events.cs | 49 ++ .../SyntaxGeneratorExtensions.Fields.cs | 39 ++ .../SyntaxGeneratorExtensions.Generics.cs | 132 ++++ .../SyntaxGeneratorExtensions.Methods.cs | 428 ++++++++++++ .../SyntaxGeneratorExtensions.Properties.cs | 86 +++ .../SyntaxGeneratorExtensions.Types.cs | 209 ++++++ ...ntaxGeneratorExtensions.UnsafeAccessors.cs | 115 ++++ .../SyntaxGeneratorExtensions.cs | 44 ++ .../CodeGeneration/TypeContext.cs | 150 +++++ .../SkeletonGeneration/MethodBodyRewriter.cs | 50 ++ .../SkeletonGeneration/SkeletonGenerator.cs | 632 ++++++++++++++++++ .../DefinitionModifiersExtensions.cs | 49 ++ .../AsmResolver/ExtendedSignatureComparer.cs | 91 +++ .../AsmResolver/GenericParameterExtensions.cs | 13 + .../AsmResolver/MemberDefinitionExtensions.cs | 88 +++ .../AsmResolver/MethodDefinitionExtensions.cs | 111 +++ .../AsmResolver/MiscellaneousExtensions.cs | 33 + ...peDefinitionExtensions.InterfaceMapping.cs | 42 ++ .../AsmResolver/TypeDefinitionExtensions.cs | 87 +++ .../AsmResolver/TypeSignatureExtensions.cs | 32 + .../Utilities/MetadataHelpers.cs | 96 +++ .../Utilities/Roslyn/TypeSymbolExtensions.cs | 62 ++ .../Utilities/StringExtensions.cs | 14 + CompatUnbreaker.Tool/packages.lock.json | 506 ++++++++++++++ CompatUnbreaker.sln | 52 ++ CompatUnbreaker/CompatUnbreaker.csproj | 25 + .../Models/Attributes/AttributeDescription.cs | 50 ++ .../AttributeDescriptionExtensions.cs | 54 ++ .../UnbreakerRenameAttributeDescription.cs | 62 ++ CompatUnbreaker/Models/ShimModel.cs | 310 +++++++++ .../Abstractions/IConsumerProcessor.cs | 8 + .../Abstractions/IReferenceProcessor.cs | 8 + .../Abstractions/ProcessorContext.cs | 8 + .../Processors/UnbreakerConsumerProcessor.cs | 389 +++++++++++ .../Processors/UnbreakerReferenceProcessor.cs | 247 +++++++ CompatUnbreaker/Unbreaker.cs | 29 + .../Utilities/AsmResolver/Accessibility.cs | 49 ++ .../AsmResolver/AccessibilityExtensions.cs | 107 +++ .../CorLibTypeFactoryExtensions.cs | 13 + .../MemberClonerLite.CustomAttributes.cs | 68 ++ .../AsmResolver/MemberClonerLite.Fields.cs | 22 + .../MemberClonerLite.GenericParameters.cs | 31 + .../AsmResolver/MemberClonerLite.Methods.cs | 41 ++ .../MemberClonerLite.SecurityDeclarations.cs | 47 ++ .../AsmResolver/MemberClonerLite.Semantics.cs | 46 ++ .../AsmResolver/MemberClonerLite.Types.cs | 42 ++ .../Utilities/AsmResolver/MemberClonerLite.cs | 44 ++ .../AsmResolver/MemberDefinitionExtensions.cs | 19 + .../AsmResolver/MethodDefinitionExtensions.cs | 17 + .../Utilities/AsmResolver/ReferenceVisitor.cs | 146 ++++ .../AsmResolver/SimpleAssemblyResolver.cs | 84 +++ .../AsmResolver/TypeDefinitionExtensions.cs | 86 +++ .../Utilities/AsmResolver/TypeMapVisitor.cs | 127 ++++ .../AsmResolver/VisibilityExtensions.cs | 43 ++ CompatUnbreaker/packages.lock.json | 141 ++++ Directory.Build.props | 16 + PlaygroundLibrary/Directory.Build.props | 10 + PlaygroundLibrary/PlaygroundLibrary.V1.csproj | 6 + PlaygroundLibrary/PlaygroundLibrary.V2.csproj | 6 + PlaygroundLibrary/TestClass.cs | 43 ++ PlaygroundLibrary/packages.lock.json | 33 + global.json | 6 + nuget.config | 18 + 115 files changed, 9009 insertions(+) create mode 100644 .github/workflows/main.yml create mode 100644 .gitignore create mode 100644 CompatUnbreaker.Attributes/CompatUnbreaker.Attributes.csproj create mode 100644 CompatUnbreaker.Attributes/NotSupportedAnymoreException.cs create mode 100644 CompatUnbreaker.Attributes/UnbreakerConstructorAttribute.cs create mode 100644 CompatUnbreaker.Attributes/UnbreakerExtensionAttribute.cs create mode 100644 CompatUnbreaker.Attributes/UnbreakerExtensionsAttribute.cs create mode 100644 CompatUnbreaker.Attributes/UnbreakerFieldAttribute.cs create mode 100644 CompatUnbreaker.Attributes/UnbreakerRenameAttribute.cs create mode 100644 CompatUnbreaker.Attributes/UnbreakerReplaceAttribute.cs create mode 100644 CompatUnbreaker.Attributes/UnbreakerShimAttribute.cs create mode 100644 CompatUnbreaker.Attributes/UnbreakerThisAttribute.cs create mode 100644 CompatUnbreaker.Attributes/packages.lock.json create mode 100644 CompatUnbreaker.Tests/AsmResolverSyntaxGeneratorTests.cs create mode 100644 CompatUnbreaker.Tests/CompatUnbreaker.Tests.csproj create mode 100644 CompatUnbreaker.Tests/TestFiles/AttributeTests.cs create mode 100644 CompatUnbreaker.Tests/TestFiles/DllImportTests.cs create mode 100644 CompatUnbreaker.Tests/TestFiles/MethodTests.cs create mode 100644 CompatUnbreaker.Tests/TestFiles/NamedTupleTests.cs create mode 100644 CompatUnbreaker.Tests/TestFiles/ReservedTests.cs create mode 100644 CompatUnbreaker.Tests/packages.lock.json create mode 100644 CompatUnbreaker.Tool/ApiCompatibility/AssemblyMapping/AssemblyMapper.cs create mode 100644 CompatUnbreaker.Tool/ApiCompatibility/AssemblyMapping/ElementMapper.cs create mode 100644 CompatUnbreaker.Tool/ApiCompatibility/AssemblyMapping/ElementSide.cs create mode 100644 CompatUnbreaker.Tool/ApiCompatibility/AssemblyMapping/MapperSettings.cs create mode 100644 CompatUnbreaker.Tool/ApiCompatibility/AssemblyMapping/MemberMapper.cs create mode 100644 CompatUnbreaker.Tool/ApiCompatibility/AssemblyMapping/TypeMapper.cs create mode 100644 CompatUnbreaker.Tool/ApiCompatibility/Comparing/ApiComparer.cs create mode 100644 CompatUnbreaker.Tool/ApiCompatibility/Comparing/CompatDifference.cs create mode 100644 CompatUnbreaker.Tool/ApiCompatibility/Comparing/DifferenceType.cs create mode 100644 CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/BaseRule.cs create mode 100644 CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotAddAbstractMember.cs create mode 100644 CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotAddMemberToInterface.cs create mode 100644 CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotAddOrRemoveVirtualKeyword.cs create mode 100644 CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotChangeGenericConstraints.cs create mode 100644 CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotChangeVisibility.cs create mode 100644 CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotRemoveBaseTypeOrInterface.cs create mode 100644 CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotSealType.cs create mode 100644 CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/EnumsMustMatch.cs create mode 100644 CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/MembersMustExist.cs create mode 100644 CompatUnbreaker.Tool/ApiCompatibility/README.md create mode 100644 CompatUnbreaker.Tool/Commands/BaseShimProjectCommand.cs create mode 100644 CompatUnbreaker.Tool/Commands/CompareCommand.cs create mode 100644 CompatUnbreaker.Tool/Commands/SkeletonCommand.cs create mode 100644 CompatUnbreaker.Tool/CompatUnbreaker.Tool.csproj create mode 100644 CompatUnbreaker.Tool/Program.cs create mode 100644 CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/AsmResolverTypeSyntaxGenerator.cs create mode 100644 CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/DeclarationModifiersExtensions.cs create mode 100644 CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/NullableAnnotation.cs create mode 100644 CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/RoslynIdentifierExtensions.cs create mode 100644 CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Attributes.cs create mode 100644 CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.EnumValues.cs create mode 100644 CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Events.cs create mode 100644 CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Fields.cs create mode 100644 CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Generics.cs create mode 100644 CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Methods.cs create mode 100644 CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Properties.cs create mode 100644 CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Types.cs create mode 100644 CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.UnsafeAccessors.cs create mode 100644 CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.cs create mode 100644 CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/TypeContext.cs create mode 100644 CompatUnbreaker.Tool/SkeletonGeneration/MethodBodyRewriter.cs create mode 100644 CompatUnbreaker.Tool/SkeletonGeneration/SkeletonGenerator.cs create mode 100644 CompatUnbreaker.Tool/Utilities/AsmResolver/DefinitionModifiersExtensions.cs create mode 100644 CompatUnbreaker.Tool/Utilities/AsmResolver/ExtendedSignatureComparer.cs create mode 100644 CompatUnbreaker.Tool/Utilities/AsmResolver/GenericParameterExtensions.cs create mode 100644 CompatUnbreaker.Tool/Utilities/AsmResolver/MemberDefinitionExtensions.cs create mode 100644 CompatUnbreaker.Tool/Utilities/AsmResolver/MethodDefinitionExtensions.cs create mode 100644 CompatUnbreaker.Tool/Utilities/AsmResolver/MiscellaneousExtensions.cs create mode 100644 CompatUnbreaker.Tool/Utilities/AsmResolver/TypeDefinitionExtensions.InterfaceMapping.cs create mode 100644 CompatUnbreaker.Tool/Utilities/AsmResolver/TypeDefinitionExtensions.cs create mode 100644 CompatUnbreaker.Tool/Utilities/AsmResolver/TypeSignatureExtensions.cs create mode 100644 CompatUnbreaker.Tool/Utilities/MetadataHelpers.cs create mode 100644 CompatUnbreaker.Tool/Utilities/Roslyn/TypeSymbolExtensions.cs create mode 100644 CompatUnbreaker.Tool/Utilities/StringExtensions.cs create mode 100644 CompatUnbreaker.Tool/packages.lock.json create mode 100644 CompatUnbreaker.sln create mode 100644 CompatUnbreaker/CompatUnbreaker.csproj create mode 100644 CompatUnbreaker/Models/Attributes/AttributeDescription.cs create mode 100644 CompatUnbreaker/Models/Attributes/AttributeDescriptionExtensions.cs create mode 100644 CompatUnbreaker/Models/Attributes/UnbreakerRenameAttributeDescription.cs create mode 100644 CompatUnbreaker/Models/ShimModel.cs create mode 100644 CompatUnbreaker/Processors/Abstractions/IConsumerProcessor.cs create mode 100644 CompatUnbreaker/Processors/Abstractions/IReferenceProcessor.cs create mode 100644 CompatUnbreaker/Processors/Abstractions/ProcessorContext.cs create mode 100644 CompatUnbreaker/Processors/UnbreakerConsumerProcessor.cs create mode 100644 CompatUnbreaker/Processors/UnbreakerReferenceProcessor.cs create mode 100644 CompatUnbreaker/Unbreaker.cs create mode 100644 CompatUnbreaker/Utilities/AsmResolver/Accessibility.cs create mode 100644 CompatUnbreaker/Utilities/AsmResolver/AccessibilityExtensions.cs create mode 100644 CompatUnbreaker/Utilities/AsmResolver/CorLibTypeFactoryExtensions.cs create mode 100644 CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.CustomAttributes.cs create mode 100644 CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.Fields.cs create mode 100644 CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.GenericParameters.cs create mode 100644 CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.Methods.cs create mode 100644 CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.SecurityDeclarations.cs create mode 100644 CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.Semantics.cs create mode 100644 CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.Types.cs create mode 100644 CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.cs create mode 100644 CompatUnbreaker/Utilities/AsmResolver/MemberDefinitionExtensions.cs create mode 100644 CompatUnbreaker/Utilities/AsmResolver/MethodDefinitionExtensions.cs create mode 100644 CompatUnbreaker/Utilities/AsmResolver/ReferenceVisitor.cs create mode 100644 CompatUnbreaker/Utilities/AsmResolver/SimpleAssemblyResolver.cs create mode 100644 CompatUnbreaker/Utilities/AsmResolver/TypeDefinitionExtensions.cs create mode 100644 CompatUnbreaker/Utilities/AsmResolver/TypeMapVisitor.cs create mode 100644 CompatUnbreaker/Utilities/AsmResolver/VisibilityExtensions.cs create mode 100644 CompatUnbreaker/packages.lock.json create mode 100644 Directory.Build.props create mode 100644 PlaygroundLibrary/Directory.Build.props create mode 100644 PlaygroundLibrary/PlaygroundLibrary.V1.csproj create mode 100644 PlaygroundLibrary/PlaygroundLibrary.V2.csproj create mode 100644 PlaygroundLibrary/TestClass.cs create mode 100644 PlaygroundLibrary/packages.lock.json create mode 100644 global.json create mode 100644 nuget.config diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..e3146e2 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,63 @@ +name: CI + +on: [ "push", "pull_request" ] + +env: + DOTNET_CLI_TELEMETRY_OPTOUT: true + DOTNET_NOLOGO: true + + # Make all dotnet builds default to Release configuration + Configuration: Release + +jobs: + build: + runs-on: ubuntu-22.04 + + env: + NUGET_ARTIFACTS: artifacts/package/release/*.nupkg + + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + filter: tree:0 + + - name: Setup .NET + uses: js6pak/setup-dotnet@8a21b4ed1527c384b6d4b7919db11dcb8389065c # https://github.com/actions/setup-dotnet/pull/538 + with: + global-json-file: global.json + + - name: Restore + run: dotnet restore --locked-mode + + - name: Build + run: dotnet build --no-restore + + - uses: actions/upload-artifact@v4 + with: + name: nuget + path: ${{ env.NUGET_ARTIFACTS }} + + - name: Format + run: dotnet format --no-restore --verbosity diagnostic --verify-no-changes + + - name: Test + run: dotnet test --no-restore --logger GitHubActions + + - name: Publish nightly + if: vars.NIGHTLY_NUGET_SOURCE != '' && (github.ref == 'refs/heads/master' || github.ref_type == 'tag') + env: + NUGET_SOURCE: ${{ vars.NIGHTLY_NUGET_SOURCE }} + NUGET_API_KEY: ${{ secrets.NIGHTLY_NUGET_API_KEY }} + run: | + dotnet nuget push --skip-duplicate $NUGET_ARTIFACTS \ + --source "$NUGET_SOURCE" --api-key "$NUGET_API_KEY" + + - name: Publish + if: github.ref_type == 'tag' + env: + NUGET_SOURCE: ${{ vars.NUGET_SOURCE || 'nuget.org' }} + NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} + run: | + dotnet nuget push --skip-duplicate $NUGET_ARTIFACTS \ + --source "$NUGET_SOURCE" --api-key "$NUGET_API_KEY" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7b83424 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Build results +bin/ +obj/ +artifacts/ + +# User-specific files +*.user +.env + +# MSBuild Binary and Structured Log +*.binlog + +# JetBrains Rider +.idea/ + +# VS Code +.vscode/ + +# Visual Studio +.vs/ + +# .editorconfig is generated by OpinionPak +/.editorconfig diff --git a/CompatUnbreaker.Attributes/CompatUnbreaker.Attributes.csproj b/CompatUnbreaker.Attributes/CompatUnbreaker.Attributes.csproj new file mode 100644 index 0000000..9f5a0cf --- /dev/null +++ b/CompatUnbreaker.Attributes/CompatUnbreaker.Attributes.csproj @@ -0,0 +1,11 @@ + + + true + net9.0;netstandard2.0;net35 + + + + + + + diff --git a/CompatUnbreaker.Attributes/NotSupportedAnymoreException.cs b/CompatUnbreaker.Attributes/NotSupportedAnymoreException.cs new file mode 100644 index 0000000..5e9970c --- /dev/null +++ b/CompatUnbreaker.Attributes/NotSupportedAnymoreException.cs @@ -0,0 +1,16 @@ +namespace CompatUnbreaker.Attributes; + +public sealed class NotSupportedAnymoreException : NotSupportedException +{ + public NotSupportedAnymoreException() + { + } + + public NotSupportedAnymoreException(string message) : base(message) + { + } + + public NotSupportedAnymoreException(string message, Exception innerException) : base(message, innerException) + { + } +} diff --git a/CompatUnbreaker.Attributes/UnbreakerConstructorAttribute.cs b/CompatUnbreaker.Attributes/UnbreakerConstructorAttribute.cs new file mode 100644 index 0000000..73b4f6b --- /dev/null +++ b/CompatUnbreaker.Attributes/UnbreakerConstructorAttribute.cs @@ -0,0 +1,4 @@ +namespace CompatUnbreaker.Attributes; + +[AttributeUsage(AttributeTargets.Method)] +public sealed class UnbreakerConstructorAttribute : Attribute; diff --git a/CompatUnbreaker.Attributes/UnbreakerExtensionAttribute.cs b/CompatUnbreaker.Attributes/UnbreakerExtensionAttribute.cs new file mode 100644 index 0000000..459e6d3 --- /dev/null +++ b/CompatUnbreaker.Attributes/UnbreakerExtensionAttribute.cs @@ -0,0 +1,9 @@ +namespace CompatUnbreaker.Attributes; + +[AttributeUsage(AttributeTargets.Class)] +public sealed class UnbreakerExtensionAttribute : Attribute +{ + public UnbreakerExtensionAttribute(Type type) + { + } +} diff --git a/CompatUnbreaker.Attributes/UnbreakerExtensionsAttribute.cs b/CompatUnbreaker.Attributes/UnbreakerExtensionsAttribute.cs new file mode 100644 index 0000000..f6e0113 --- /dev/null +++ b/CompatUnbreaker.Attributes/UnbreakerExtensionsAttribute.cs @@ -0,0 +1,4 @@ +namespace CompatUnbreaker.Attributes; + +[AttributeUsage(AttributeTargets.Class)] +public sealed class UnbreakerExtensionsAttribute : Attribute; diff --git a/CompatUnbreaker.Attributes/UnbreakerFieldAttribute.cs b/CompatUnbreaker.Attributes/UnbreakerFieldAttribute.cs new file mode 100644 index 0000000..e9564ca --- /dev/null +++ b/CompatUnbreaker.Attributes/UnbreakerFieldAttribute.cs @@ -0,0 +1,4 @@ +namespace CompatUnbreaker.Attributes; + +[AttributeUsage(AttributeTargets.Property)] +public sealed class UnbreakerFieldAttribute : Attribute; diff --git a/CompatUnbreaker.Attributes/UnbreakerRenameAttribute.cs b/CompatUnbreaker.Attributes/UnbreakerRenameAttribute.cs new file mode 100644 index 0000000..bfe19eb --- /dev/null +++ b/CompatUnbreaker.Attributes/UnbreakerRenameAttribute.cs @@ -0,0 +1,45 @@ +namespace CompatUnbreaker.Attributes; + +[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] +public sealed class UnbreakerRenameAttribute : Attribute +{ + public UnbreakerRenameAttribute(string namespaceName, string newNamespaceName) + { + } + + public UnbreakerRenameAttribute(Type type, string newTypeName) + { + } + + public UnbreakerRenameAttribute(Type type, string memberName, string newMemberName) + { + } +} + +// TODO maybe switch to this? +public static class UnbreakerRename +{ + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class NamespaceAttribute : Attribute + { + public NamespaceAttribute(string name, string newName) + { + } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class TypeAttribute : Attribute + { + public TypeAttribute(Type type, string newName) + { + } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class TypeMemberAttribute : Attribute + { + public TypeMemberAttribute(Type type, string memberName, string newMemberName) + { + } + } +} diff --git a/CompatUnbreaker.Attributes/UnbreakerReplaceAttribute.cs b/CompatUnbreaker.Attributes/UnbreakerReplaceAttribute.cs new file mode 100644 index 0000000..2a3a9a3 --- /dev/null +++ b/CompatUnbreaker.Attributes/UnbreakerReplaceAttribute.cs @@ -0,0 +1,9 @@ +namespace CompatUnbreaker.Attributes; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] +public sealed class UnbreakerReplaceAttribute : Attribute +{ + public UnbreakerReplaceAttribute(Type type) + { + } +} diff --git a/CompatUnbreaker.Attributes/UnbreakerShimAttribute.cs b/CompatUnbreaker.Attributes/UnbreakerShimAttribute.cs new file mode 100644 index 0000000..4430511 --- /dev/null +++ b/CompatUnbreaker.Attributes/UnbreakerShimAttribute.cs @@ -0,0 +1,9 @@ +namespace CompatUnbreaker.Attributes; + +[AttributeUsage(AttributeTargets.Assembly)] +public sealed class UnbreakerShimAttribute : Attribute +{ + public UnbreakerShimAttribute(string targetAssemblyName) + { + } +} diff --git a/CompatUnbreaker.Attributes/UnbreakerThisAttribute.cs b/CompatUnbreaker.Attributes/UnbreakerThisAttribute.cs new file mode 100644 index 0000000..119e150 --- /dev/null +++ b/CompatUnbreaker.Attributes/UnbreakerThisAttribute.cs @@ -0,0 +1,7 @@ +namespace CompatUnbreaker.Attributes; + +/// +/// A substitute for the keyword, to allow shimming extension methods. +/// +[AttributeUsage(AttributeTargets.Parameter)] +public sealed class UnbreakerThisAttribute : Attribute; diff --git a/CompatUnbreaker.Attributes/packages.lock.json b/CompatUnbreaker.Attributes/packages.lock.json new file mode 100644 index 0000000..29e7a20 --- /dev/null +++ b/CompatUnbreaker.Attributes/packages.lock.json @@ -0,0 +1,117 @@ +{ + "version": 1, + "dependencies": { + ".NETFramework,Version=v3.5": { + "Meziantou.Analyzer": { + "type": "Direct", + "requested": "[2.0.220, )", + "resolved": "2.0.220", + "contentHash": "LknH5tduuxY6jzPGUIBHrszk1fepyEWAD9Ht7Hq1rrGZ02BKx54ADXzrlXTkLNpnkVzRBHU4bO6HH7GwIeDR+w==" + }, + "Microsoft.NETFramework.ReferenceAssemblies": { + "type": "Direct", + "requested": "[1.0.3, )", + "resolved": "1.0.3", + "contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==", + "dependencies": { + "Microsoft.NETFramework.ReferenceAssemblies.net35": "1.0.3" + } + }, + "PolySharp": { + "type": "Direct", + "requested": "[1.15.0, )", + "resolved": "1.15.0", + "contentHash": "FbU0El+EEjdpuIX4iDbeS7ki1uzpJPx8vbqOzEtqnl1GZeAGJfq+jCbxeJL2y0EPnUNk8dRnnqR2xnYXg9Tf+g==" + }, + "StyleCop.Analyzers": { + "type": "Direct", + "requested": "[1.2.0-beta.556, )", + "resolved": "1.2.0-beta.556", + "contentHash": "llRPgmA1fhC0I0QyFLEcjvtM2239QzKr/tcnbsjArLMJxJlu0AA5G7Fft0OI30pHF3MW63Gf4aSSsjc5m82J1Q==", + "dependencies": { + "StyleCop.Analyzers.Unstable": "1.2.0.556" + } + }, + "Microsoft.NETFramework.ReferenceAssemblies.net35": { + "type": "Transitive", + "resolved": "1.0.3", + "contentHash": "Mhbr13IGgsW4AHj50uAiVNMPWdvUPWePRsI4x1NiTkhHUc4rqi9XvY5xxBC4d56bJypQbxAZ8bPuZIpz+TjBVQ==" + }, + "StyleCop.Analyzers.Unstable": { + "type": "Transitive", + "resolved": "1.2.0.556", + "contentHash": "zvn9Mqs/ox/83cpYPignI8hJEM2A93s2HkHs8HYMOAQW0PkampyoErAiIyKxgTLqbbad29HX/shv/6LGSjPJNQ==" + } + }, + ".NETStandard,Version=v2.0": { + "Meziantou.Analyzer": { + "type": "Direct", + "requested": "[2.0.220, )", + "resolved": "2.0.220", + "contentHash": "LknH5tduuxY6jzPGUIBHrszk1fepyEWAD9Ht7Hq1rrGZ02BKx54ADXzrlXTkLNpnkVzRBHU4bO6HH7GwIeDR+w==" + }, + "NETStandard.Library": { + "type": "Direct", + "requested": "[2.0.3, )", + "resolved": "2.0.3", + "contentHash": "st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0" + } + }, + "PolySharp": { + "type": "Direct", + "requested": "[1.15.0, )", + "resolved": "1.15.0", + "contentHash": "FbU0El+EEjdpuIX4iDbeS7ki1uzpJPx8vbqOzEtqnl1GZeAGJfq+jCbxeJL2y0EPnUNk8dRnnqR2xnYXg9Tf+g==" + }, + "StyleCop.Analyzers": { + "type": "Direct", + "requested": "[1.2.0-beta.556, )", + "resolved": "1.2.0-beta.556", + "contentHash": "llRPgmA1fhC0I0QyFLEcjvtM2239QzKr/tcnbsjArLMJxJlu0AA5G7Fft0OI30pHF3MW63Gf4aSSsjc5m82J1Q==", + "dependencies": { + "StyleCop.Analyzers.Unstable": "1.2.0.556" + } + }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" + }, + "StyleCop.Analyzers.Unstable": { + "type": "Transitive", + "resolved": "1.2.0.556", + "contentHash": "zvn9Mqs/ox/83cpYPignI8hJEM2A93s2HkHs8HYMOAQW0PkampyoErAiIyKxgTLqbbad29HX/shv/6LGSjPJNQ==" + } + }, + "net9.0": { + "Meziantou.Analyzer": { + "type": "Direct", + "requested": "[2.0.220, )", + "resolved": "2.0.220", + "contentHash": "LknH5tduuxY6jzPGUIBHrszk1fepyEWAD9Ht7Hq1rrGZ02BKx54ADXzrlXTkLNpnkVzRBHU4bO6HH7GwIeDR+w==" + }, + "PolySharp": { + "type": "Direct", + "requested": "[1.15.0, )", + "resolved": "1.15.0", + "contentHash": "FbU0El+EEjdpuIX4iDbeS7ki1uzpJPx8vbqOzEtqnl1GZeAGJfq+jCbxeJL2y0EPnUNk8dRnnqR2xnYXg9Tf+g==" + }, + "StyleCop.Analyzers": { + "type": "Direct", + "requested": "[1.2.0-beta.556, )", + "resolved": "1.2.0-beta.556", + "contentHash": "llRPgmA1fhC0I0QyFLEcjvtM2239QzKr/tcnbsjArLMJxJlu0AA5G7Fft0OI30pHF3MW63Gf4aSSsjc5m82J1Q==", + "dependencies": { + "StyleCop.Analyzers.Unstable": "1.2.0.556" + } + }, + "StyleCop.Analyzers.Unstable": { + "type": "Transitive", + "resolved": "1.2.0.556", + "contentHash": "zvn9Mqs/ox/83cpYPignI8hJEM2A93s2HkHs8HYMOAQW0PkampyoErAiIyKxgTLqbbad29HX/shv/6LGSjPJNQ==" + } + } + } +} \ No newline at end of file diff --git a/CompatUnbreaker.Tests/AsmResolverSyntaxGeneratorTests.cs b/CompatUnbreaker.Tests/AsmResolverSyntaxGeneratorTests.cs new file mode 100644 index 0000000..fe82367 --- /dev/null +++ b/CompatUnbreaker.Tests/AsmResolverSyntaxGeneratorTests.cs @@ -0,0 +1,172 @@ +using System.Diagnostics.CodeAnalysis; +using AsmResolver.DotNet; +using CompatUnbreaker.Tool.SkeletonGeneration.CodeGeneration; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Simplification; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using Assembly = System.Reflection.Assembly; + +namespace CompatUnbreaker.Tests; + +public sealed class AsmResolverSyntaxGeneratorTests +{ + public static IEnumerable<(string FileName, string Source)> DataSource() + { + var assembly = Assembly.GetExecutingAssembly(); + var names = assembly.GetManifestResourceNames(); + + const string Prefix = "CompatUnbreaker.Tests.TestFiles."; + + foreach (var name in names) + { + if (!name.StartsWith(Prefix, StringComparison.Ordinal)) continue; + + var fileName = name[Prefix.Length..]; + using var streamReader = new StreamReader(assembly.GetManifestResourceStream(name)!); + var source = streamReader.ReadToEnd(); + + yield return (fileName, source); + } + } + + [Test] + [MethodDataSource(nameof(DataSource))] + [DisplayName("Roundtrip($fileName)")] + public async Task Roundtrip(string fileName, string source) + { + var bytes = Compile(source, out var sourceSyntaxTree); + + var assemblyDefinition = AssemblyDefinition.FromBytes(bytes); + + using var workspace = new AdhocWorkspace(); + var project = workspace.AddProject("Test", LanguageNames.CSharp) + .WithMetadataReferences(Basic.Reference.Assemblies.Net90.References.All) + .WithCompilationOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary) + .WithOptimizationLevel(OptimizationLevel.Release) + .WithAllowUnsafe(true) + .WithNullableContextOptions(NullableContextOptions.Enable)); + + project = project.AddDocument("GlobalUsings.g.cs", GlobalUsingsText).Project; + + var addedDocuments = new List(); + + foreach (var module in assemblyDefinition.Modules) + { + foreach (var type in module.TopLevelTypes) + { + if (type.IsModuleType || type.IsCompilerGenerated()) continue; + + var syntaxGenerator = SyntaxGenerator.GetGenerator(project); + var member = (MemberDeclarationSyntax) syntaxGenerator.Declaration(type); + + if (type.Namespace is { } @namespace) + { + member = FileScopedNamespaceDeclaration((NameSyntax) syntaxGenerator.DottedName(@namespace)) + .AddMembers(member); + } + + var syntaxRoot = CompilationUnit() + .AddMembers(member) + .WithTrailingTrivia(LineFeed) + .WithAdditionalAnnotations(Simplifier.Annotation, Simplifier.AddImportsAnnotation); + + var document = project.AddDocument("TestClass", syntaxRoot); + addedDocuments.Add(document.Id); + project = document.Project; + } + } + + foreach (var documentId in addedDocuments) + { + var document = project.GetDocument(documentId); + + document = await ImportAdder.AddImportsAsync(document); + document = await Simplifier.ReduceAsync(document); + document = await Formatter.FormatAsync(document); + + var text = (await document.GetTextAsync()).ToString(); + Console.WriteLine(text); + + var syntaxTree = await document.GetSyntaxTreeAsync(); + ArgumentNullException.ThrowIfNull(syntaxTree); + + if (!sourceSyntaxTree.IsEquivalentTo(syntaxTree)) + { + await Assert.That(ToStringWithoutTrivia(syntaxTree)).IsEqualTo(ToStringWithoutTrivia(sourceSyntaxTree)); + } + + project = document.Project; + } + } + + private static string ToStringWithoutTrivia(SyntaxTree tree) + { + var syntaxNode = tree.GetRoot(); + syntaxNode = TriviaRemover.Instance.Visit(syntaxNode); + return syntaxNode.NormalizeWhitespace().ToFullString(); + } + + private sealed class TriviaRemover : CSharpSyntaxRewriter + { + public static TriviaRemover Instance { get; } = new(); + + [return: NotNullIfNotNull(nameof(node))] + public override SyntaxNode? Visit(SyntaxNode? node) + { + return base.Visit(node)?.WithoutTrivia(); + } + + public override SyntaxTrivia VisitTrivia(SyntaxTrivia trivia) + { + return SyntaxFactory.Whitespace(""); + } + } + + [StringSyntax(LanguageNames.CSharp)] + private const string GlobalUsingsText = + """ + // + global using System; + global using System.Collections.Generic; + global using System.IO; + global using System.Linq; + global using System.Net.Http; + global using System.Threading; + global using System.Threading.Tasks; + """; + + private static byte[] Compile([StringSyntax(LanguageNames.CSharp)] string source, out SyntaxTree syntaxTree) + { + var compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary) + .WithOptimizationLevel(OptimizationLevel.Release) + .WithAllowUnsafe(true) + .WithNullableContextOptions(NullableContextOptions.Enable); + + var references = Basic.Reference.Assemblies.Net90.References.All; + + syntaxTree = ParseSyntaxTree(source); + + var globalUsings = ParseSyntaxTree(GlobalUsingsText); + + var compilation = CSharpCompilation.Create("Test", [globalUsings, syntaxTree], references, compilationOptions); + + var stream = new MemoryStream(); + + var emitResult = compilation.Emit(stream); + if (!emitResult.Success) + { + foreach (var diagnostic in emitResult.Diagnostics) + { + Console.WriteLine(diagnostic); + } + + throw new Exception(); + } + + return stream.GetBuffer(); + } +} diff --git a/CompatUnbreaker.Tests/CompatUnbreaker.Tests.csproj b/CompatUnbreaker.Tests/CompatUnbreaker.Tests.csproj new file mode 100644 index 0000000..583d837 --- /dev/null +++ b/CompatUnbreaker.Tests/CompatUnbreaker.Tests.csproj @@ -0,0 +1,21 @@ + + + net$(NETCoreAppMaximumVersion) + Exe + true + + + + + + + + + + + + + + + + diff --git a/CompatUnbreaker.Tests/TestFiles/AttributeTests.cs b/CompatUnbreaker.Tests/TestFiles/AttributeTests.cs new file mode 100644 index 0000000..544480a --- /dev/null +++ b/CompatUnbreaker.Tests/TestFiles/AttributeTests.cs @@ -0,0 +1,80 @@ +namespace CompatUnbreaker.Tests.TestFiles; + +[Test("string")] +[Test(new string[] { "a", "b" })] +[Test(123)] +[Test(typeof(AttributeTests))] +[Test(typeof(Action<,>))] +// [Test(TestEnum.B)] TODO this requires porting CSharpFlagsEnumGenerator +[Test((object?) null)] +[Test((string?) null)] +[Test((string[]?) null)] +[Test((Type?) null)] +public sealed class AttributeTests +{ + public const AttributeTargets Guh = AttributeTargets.Assembly | AttributeTargets.Interface | AttributeTargets.Delegate; + + [Test] + public int field; + + [Test] + public extern int Property { get; set; } + + [Test] + public extern event Action Event; + + [Test] + public AttributeTests() + { + } + + [Test] + [return: Test] + public static extern T Method<[Test] T>([Test] T parameter); + + [Test] + public delegate T Delegate(); + + // [Test] + // public interface IInterface + // { + // } + + [Test] + public enum TestEnum : byte + { + A = 1, + B = 2 + } + + [Test] + public struct TestStruct; + + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] + public sealed class TestAttribute : Attribute + { + public TestAttribute() + { + } + + public TestAttribute(string? a) + { + } + + public TestAttribute(string[]? a) + { + } + + public TestAttribute(object? a) + { + } + + public TestAttribute(Type? a) + { + } + + public TestAttribute(TestEnum a) + { + } + } +} diff --git a/CompatUnbreaker.Tests/TestFiles/DllImportTests.cs b/CompatUnbreaker.Tests/TestFiles/DllImportTests.cs new file mode 100644 index 0000000..bad0e7f --- /dev/null +++ b/CompatUnbreaker.Tests/TestFiles/DllImportTests.cs @@ -0,0 +1,12 @@ +using System.Runtime.InteropServices; + +namespace CompatUnbreaker.Tests.TestFiles; + +public static class DllImportTests +{ + [DllImport("cat")] + public static extern void Meow(); + + [DllImport("cat", EntryPoint = "meow", CharSet = CharSet.Ansi, SetLastError = true, ExactSpelling = true, CallingConvention = CallingConvention.FastCall, BestFitMapping = false, PreserveSig = false, ThrowOnUnmappableChar = false)] + public static extern void MeowFull(); +} diff --git a/CompatUnbreaker.Tests/TestFiles/MethodTests.cs b/CompatUnbreaker.Tests/TestFiles/MethodTests.cs new file mode 100644 index 0000000..f9c2556 --- /dev/null +++ b/CompatUnbreaker.Tests/TestFiles/MethodTests.cs @@ -0,0 +1,26 @@ +using System.Diagnostics.CodeAnalysis; + +namespace CompatUnbreaker.Tests.TestFiles; + +public sealed class MethodTests +{ + public static extern void Method(); + + public static extern ref readonly int AllTheRefs(ref int @ref, ref readonly int refReadonly, in int @in, out int @out, scoped ref int scopedRef, [UnscopedRef] ref int unscopedRef, scoped Span scopedSpan); + + public static extern void ScopedGeneric(scoped T a) where T : unmanaged, allows ref struct; + + public static extern T Generic(T t) where T : class, IDisposable, new(); + + public static extern void Array(int[] a, int[][] b, int[,] c); + + public static extern void Params(params int[] a); + + public static extern void ParamsSpan(params ReadOnlySpan a); + + public static extern void Default(int a = 123); + + // TODO needs RequiresUnsafeModifier implemented + // public static extern unsafe void Pointer(int* a); + // public static extern unsafe void FunctionPointer(delegate* a, delegate* unmanaged[Stdcall, SuppressGCTransition] b); +} diff --git a/CompatUnbreaker.Tests/TestFiles/NamedTupleTests.cs b/CompatUnbreaker.Tests/TestFiles/NamedTupleTests.cs new file mode 100644 index 0000000..deeff73 --- /dev/null +++ b/CompatUnbreaker.Tests/TestFiles/NamedTupleTests.cs @@ -0,0 +1,14 @@ +namespace CompatUnbreaker.Tests.TestFiles; + +public sealed class NamedTupleTests +{ + public + ( + (object? aa1, object aa2) a1, object a2, object a3, object a4, object a5, object a6, object a7, object a8, + object b1, (object ba1, object ba2) b2, object b3, object b4, object? b5, object b6, object b7, object b8, + object c1, object c2, (object ca1, object ca2) c3, object c4, object c5, object c6, object c7, object c8, + object d1, object d2, object d3, (object da1, object? da2) d4, object d5, object d6, object d7, object d8 + ) longgg; + + public (object, object) unnamed; +} diff --git a/CompatUnbreaker.Tests/TestFiles/ReservedTests.cs b/CompatUnbreaker.Tests/TestFiles/ReservedTests.cs new file mode 100644 index 0000000..08e4d5c --- /dev/null +++ b/CompatUnbreaker.Tests/TestFiles/ReservedTests.cs @@ -0,0 +1,28 @@ +namespace CompatUnbreaker.Tests.TestFiles; + +public static class ReservedTests +{ + public static extern dynamic Dynamic(dynamic a, dynamic[] b, (dynamic, object, int, dynamic) c); + + public static extern ref readonly int ReadOnly(); + + public static extern void RequiredLocation(ref readonly int a); + + public static extern void IsUnmanaged() where T : unmanaged; + + public static extern (int A, ((int E, int F) C, int D) B) TupleElementNames(); + + public static extern object? Nullable(object? a, object b, object? c, object d, T? e, object?[][]? f); + + public static extern void Extension(this object o); + + public ref struct IsByRefLike; + + public class Generics + where A : notnull + where B : class + where C : class? + { + public required A a; + } +} diff --git a/CompatUnbreaker.Tests/packages.lock.json b/CompatUnbreaker.Tests/packages.lock.json new file mode 100644 index 0000000..6f3e4c5 --- /dev/null +++ b/CompatUnbreaker.Tests/packages.lock.json @@ -0,0 +1,567 @@ +{ + "version": 1, + "dependencies": { + "net9.0": { + "Meziantou.Analyzer": { + "type": "Direct", + "requested": "[2.0.220, )", + "resolved": "2.0.220", + "contentHash": "LknH5tduuxY6jzPGUIBHrszk1fepyEWAD9Ht7Hq1rrGZ02BKx54ADXzrlXTkLNpnkVzRBHU4bO6HH7GwIeDR+w==" + }, + "PolySharp": { + "type": "Direct", + "requested": "[1.15.0, )", + "resolved": "1.15.0", + "contentHash": "FbU0El+EEjdpuIX4iDbeS7ki1uzpJPx8vbqOzEtqnl1GZeAGJfq+jCbxeJL2y0EPnUNk8dRnnqR2xnYXg9Tf+g==" + }, + "StyleCop.Analyzers": { + "type": "Direct", + "requested": "[1.2.0-beta.556, )", + "resolved": "1.2.0-beta.556", + "contentHash": "llRPgmA1fhC0I0QyFLEcjvtM2239QzKr/tcnbsjArLMJxJlu0AA5G7Fft0OI30pHF3MW63Gf4aSSsjc5m82J1Q==", + "dependencies": { + "StyleCop.Analyzers.Unstable": "1.2.0.556" + } + }, + "TUnit": { + "type": "Direct", + "requested": "[0.75.30, )", + "resolved": "0.75.30", + "contentHash": "ms5ssGbyPCA2cgqjpjXDH51EjuIkVtdiI0mH5v95bfn0lYVCaPD9CWP213Gm8tUkpE3enHIEGH5M2iQ85IyHIQ==", + "dependencies": { + "TUnit.Assertions": "0.75.30", + "TUnit.Engine": "0.75.30" + } + }, + "AsmResolver": { + "type": "Transitive", + "resolved": "6.0.0-dev" + }, + "AsmResolver.DotNet": { + "type": "Transitive", + "resolved": "6.0.0-dev", + "dependencies": { + "AsmResolver.PE": "6.0.0-dev" + } + }, + "AsmResolver.PE": { + "type": "Transitive", + "resolved": "6.0.0-dev", + "dependencies": { + "AsmResolver.PE.File": "6.0.0-dev" + } + }, + "AsmResolver.PE.File": { + "type": "Transitive", + "resolved": "6.0.0-dev", + "dependencies": { + "AsmResolver": "6.0.0-dev" + } + }, + "Basic.Reference.Assemblies.Net90": { + "type": "Transitive", + "resolved": "1.8.3", + "contentHash": "sZRH0NW/jdLTNq8X2/IVGIUVVE0JLKxwSp9GOwKzoHzt7BK5U7iRqG25ddSsggdABpvyz3ooQ4GkwQQ/Kn+z+g==", + "dependencies": { + "Microsoft.CodeAnalysis.Common": "4.11.0" + } + }, + "EnumerableAsyncProcessor": { + "type": "Transitive", + "resolved": "3.8.4", + "contentHash": "KlbpupRCz3Kf+P7gsiDvFXJ980i/9lfihMZFmmxIk0Gf6mopEjy74OTJZmdaKDQpE29eQDBnMZB5khyW3eugrg==" + }, + "Humanizer.Core": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==" + }, + "Microsoft.Build": { + "type": "Transitive", + "resolved": "17.14.28", + "contentHash": "MmGLEsROW1C9dH/d4sOqUX0sVNs2uwTCFXRQb89+pYNWDNJE+7bTJG9kOCbHeCH252XLnP55KIaOgwSpf6J4Kw==", + "dependencies": { + "Microsoft.Build.Framework": "17.14.28", + "Microsoft.NET.StringTools": "17.14.28", + "System.Configuration.ConfigurationManager": "9.0.0", + "System.Diagnostics.EventLog": "9.0.0", + "System.Reflection.MetadataLoadContext": "9.0.0", + "System.Security.Cryptography.ProtectedData": "9.0.0" + } + }, + "Microsoft.Build.Framework": { + "type": "Transitive", + "resolved": "17.14.28", + "contentHash": "wRcyTzGV0LRAtFdrddtioh59Ky4/zbvyraP0cQkDzRSRkhgAQb0K88D/JNC6VHLIXanRi3mtV1jU0uQkBwmiVg==" + }, + "Microsoft.Build.Locator": { + "type": "Transitive", + "resolved": "1.10.2", + "contentHash": "F+nLS7IpgtslyxNvtD6Jalnf5WU08lu8yfJBNQl3cbEF3AMUphs4t7nPuRYaaU8QZyGrqtVi7i73LhAe/yHx7A==" + }, + "Microsoft.Build.Tasks.Core": { + "type": "Transitive", + "resolved": "17.14.28", + "contentHash": "jk3O0tXp9QWPXhLJ7Pl8wm/eGtGgA1++vwHGWEmnwMU6eP//ghtcCUpQh9CQMwEKGDnH0aJf285V1s8yiSlKfQ==", + "dependencies": { + "Microsoft.Build.Framework": "17.14.28", + "Microsoft.Build.Utilities.Core": "17.14.28", + "Microsoft.NET.StringTools": "17.14.28", + "System.CodeDom": "9.0.0", + "System.Collections.Immutable": "9.0.0", + "System.Configuration.ConfigurationManager": "9.0.0", + "System.Diagnostics.EventLog": "9.0.0", + "System.Formats.Nrbf": "9.0.0", + "System.Resources.Extensions": "9.0.0", + "System.Security.Cryptography.Pkcs": "9.0.0", + "System.Security.Cryptography.ProtectedData": "9.0.0", + "System.Security.Cryptography.Xml": "9.0.0" + } + }, + "Microsoft.Build.Utilities.Core": { + "type": "Transitive", + "resolved": "17.14.28", + "contentHash": "rhSdPo8QfLXXWM+rY0x0z1G4KK4ZhMoIbHROyDj8MUBFab9nvHR0NaMnjzOgXldhmD2zi2ir8d6xCatNzlhF5g==", + "dependencies": { + "Microsoft.Build.Framework": "17.14.28", + "Microsoft.NET.StringTools": "17.14.28", + "System.Collections.Immutable": "9.0.0", + "System.Configuration.ConfigurationManager": "9.0.0", + "System.Diagnostics.EventLog": "9.0.0", + "System.Security.Cryptography.ProtectedData": "9.0.0" + } + }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Transitive", + "resolved": "3.11.0", + "contentHash": "v/EW3UE8/lbEYHoC2Qq7AR/DnmvpgdtAMndfQNmpuIMx/Mto8L5JnuCfdBYtgvalQOtfNCnxFejxuRrryvUTsg==" + }, + "Microsoft.CodeAnalysis.Common": { + "type": "Transitive", + "resolved": "4.14.0", + "contentHash": "PC3tuwZYnC+idaPuoC/AZpEdwrtX7qFpmnrfQkgobGIWiYmGi5MCRtl5mx6QrfMGQpK78X2lfIEoZDLg/qnuHg==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "System.Collections.Immutable": "9.0.0", + "System.Reflection.Metadata": "9.0.0" + } + }, + "Microsoft.CodeAnalysis.CSharp": { + "type": "Transitive", + "resolved": "4.14.0", + "contentHash": "568a6wcTivauIhbeWcCwfWwIn7UV7MeHEBvFB2uzGIpM2OhJ4eM/FZ8KS0yhPoNxnSpjGzz7x7CIjTxhslojQA==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Common": "[4.14.0]", + "System.Collections.Immutable": "9.0.0", + "System.Reflection.Metadata": "9.0.0" + } + }, + "Microsoft.CodeAnalysis.CSharp.Workspaces": { + "type": "Transitive", + "resolved": "4.14.0", + "contentHash": "QkgCEM4qJo6gdtblXtNgHqtykS61fxW+820hx5JN6n9DD4mQtqNB+6fPeJ3GQWg6jkkGz6oG9yZq7H3Gf0zwYw==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.CSharp": "[4.14.0]", + "Microsoft.CodeAnalysis.Common": "[4.14.0]", + "Microsoft.CodeAnalysis.Workspaces.Common": "[4.14.0]", + "System.Collections.Immutable": "9.0.0", + "System.Composition": "9.0.0", + "System.IO.Pipelines": "9.0.0", + "System.Reflection.Metadata": "9.0.0", + "System.Threading.Channels": "7.0.0" + } + }, + "Microsoft.CodeAnalysis.Workspaces.Common": { + "type": "Transitive", + "resolved": "4.14.0", + "contentHash": "wNVK9JrqjqDC/WgBUFV6henDfrW87NPfo98nzah/+M/G1D6sBOPtXwqce3UQNn+6AjTnmkHYN1WV9XmTlPemTw==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Common": "[4.14.0]", + "System.Collections.Immutable": "9.0.0", + "System.Composition": "9.0.0", + "System.IO.Pipelines": "9.0.0", + "System.Reflection.Metadata": "9.0.0", + "System.Threading.Channels": "7.0.0" + } + }, + "Microsoft.CodeAnalysis.Workspaces.MSBuild": { + "type": "Transitive", + "resolved": "4.14.0", + "contentHash": "YU7Sguzm1Cuhi2U6S0DRKcVpqAdBd2QmatpyE0KqYMJogJ9E27KHOWGUzAOjsyjAM7sNaUk+a8VPz24knDseFw==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.Build": "17.7.2", + "Microsoft.Build.Framework": "17.7.2", + "Microsoft.Build.Tasks.Core": "17.7.2", + "Microsoft.Build.Utilities.Core": "17.7.2", + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Workspaces.Common": "[4.14.0]", + "Microsoft.Extensions.DependencyInjection": "9.0.0", + "Microsoft.Extensions.Logging": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0", + "Microsoft.Extensions.Primitives": "9.0.0", + "Newtonsoft.Json": "13.0.3", + "System.CodeDom": "7.0.0", + "System.Collections.Immutable": "9.0.0", + "System.Composition": "9.0.0", + "System.Configuration.ConfigurationManager": "9.0.0", + "System.Diagnostics.EventLog": "9.0.0", + "System.IO.Pipelines": "9.0.0", + "System.Reflection.Metadata": "9.0.0", + "System.Resources.Extensions": "9.0.0", + "System.Security.Cryptography.Pkcs": "7.0.2", + "System.Security.Cryptography.ProtectedData": "9.0.0", + "System.Security.Cryptography.Xml": "7.0.1", + "System.Security.Permissions": "9.0.0", + "System.Text.Json": "9.0.0", + "System.Threading.Channels": "7.0.0", + "System.Threading.Tasks.Dataflow": "9.0.0", + "System.Windows.Extensions": "9.0.0" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "MCPrg7v3QgNMr0vX4vzRXvkNGgLg8vKWX0nKCWUxu2uPyMsaRgiRc1tHBnbTcfJMhMKj2slE/j2M9oGkd25DNw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "+6f2qv2a3dLwd5w6JanPIPs47CxRbnk+ZocMJUhv9NxP88VlOcJYZs9jY+MYSjxvady08bUZn6qgiNh7DadGgg==" + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "crjWyORoug0kK7RSNJBTeSE6VX8IQgLf3nUpTB9m62bPXp/tzbnOsnbe8TXEG0AASNaKZddnpHKw7fET8E++Pg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "g0UfujELzlLbHoVG8kPKVBaW470Ewi+jnptGS9KUi6jcb+k2StujtK3m26DFSGGwQ/+bVgZfsWqNzlP6YOejvw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0" + } + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "y2146b3jrPI3Q0lokKXdKLpmXqakYbDIPDV6r3M8SqvSf45WwOTzkyfDpxnZXJsJQEpAsAqjUq5Pu8RCJMjubg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "Microsoft.Extensions.Primitives": "9.0.0" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "N3qEBzmLMYiASUlKxxFIISP4AiwuPTHF5uCh+2CWSwwzAJiIYx0kBJsS30cp1nvhSySFAVi30jecD307jV+8Kg==" + }, + "Microsoft.NET.StringTools": { + "type": "Transitive", + "resolved": "17.14.28", + "contentHash": "DMIeWDlxe0Wz0DIhJZ2FMoGQAN2yrGZOi5jjFhRYHWR5ONd0CS6IpAHlRnA7uA/5BF+BADvgsETxW2XrPiFc1A==" + }, + "Microsoft.Testing.Extensions.TrxReport.Abstractions": { + "type": "Transitive", + "resolved": "2.0.1", + "contentHash": "GCyRVLdFiOlGkgZItt2kHJRn/NY9zhEF5EQwjFCOl6fwr+uSy0KtwwGHF5uERevjEEBTqRbj4wwrraAqRyq1Zw==", + "dependencies": { + "Microsoft.Testing.Platform": "2.0.1" + } + }, + "Microsoft.Testing.Platform": { + "type": "Transitive", + "resolved": "2.0.1", + "contentHash": "k1k9DV/pzkhiG0LOokmrBf63BBhgUoHcjsOA6C0S26hiO+prcUIw4wpbzrUGrwMmJlo1P0idxJtiFEAZdvNwxQ==" + }, + "Microsoft.Testing.Platform.MSBuild": { + "type": "Transitive", + "resolved": "2.0.1", + "contentHash": "zI8svKM2d1wAu20u+cMRo13oJv4uyPcCmVZixvKk+W34d1F7hH2msEzyQ4zv3NEliE60JMM9cdDxWcA3yU2oAA==", + "dependencies": { + "Microsoft.Testing.Platform": "2.0.1" + } + }, + "Mono.Cecil": { + "type": "Transitive", + "resolved": "0.11.6", + "contentHash": "f33RkDtZO8VlGXCtmQIviOtxgnUdym9xx/b1p9h91CRGOsJFxCFOFK1FDbVt1OCf1aWwYejUFa2MOQyFWTFjbA==" + }, + "MonoMod.Backports": { + "type": "Transitive", + "resolved": "1.1.2", + "contentHash": "baYlNy8n8kmaNhNvqmZ/dIPOeO1r9//dG1i2WbunMWtWZ2EKtIgmXaS+ZzphzTsikkGnoD4Jwr5g0TVdpDjgpw==", + "dependencies": { + "MonoMod.ILHelpers": "1.1.0" + } + }, + "MonoMod.Core": { + "type": "Transitive", + "resolved": "1.3.1", + "contentHash": "AE78s2Iv74mFfkbH+iUAGmVpf+zkMlJYOtQPVVuRN4Eorcy7Dm4wps1X/CVp4p6a7MnbCKOuQz9OeVC/xJN6+Q==", + "dependencies": { + "Mono.Cecil": "0.11.6", + "MonoMod.Backports": "1.1.2", + "MonoMod.ILHelpers": "1.1.0", + "MonoMod.Utils": "25.0.9" + } + }, + "MonoMod.ILHelpers": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "L2FWjhTrv7tcIxshfZ+M3OcaNr4cNw0IwiVZEgwqRnZ5QAN3+RrNJ8ZwCzwXUWyPDqooJxMcjjg8PsSYUiNBjQ==" + }, + "MonoMod.RuntimeDetour": { + "type": "Transitive", + "resolved": "25.3.1", + "contentHash": "QQ9Ng3E6enCMbcFNDSUqWBD4+KdnjfxConI8QzMAH9p7i2vvzehqYT8K4Mghq2YeTIVN6Aih8PlqcQxQk2uypQ==", + "dependencies": { + "Mono.Cecil": "0.11.6", + "MonoMod.Backports": "1.1.2", + "MonoMod.Core": "1.3.1", + "MonoMod.ILHelpers": "1.1.0", + "MonoMod.Utils": "25.0.9" + } + }, + "MonoMod.Utils": { + "type": "Transitive", + "resolved": "25.0.9", + "contentHash": "KPZ/VAr4zwT12/OJPZF2uazc9M/tRjiizeErq4m5Ie4EA6XnreakLfs8RlvxszgwXL7FVGnkg9JU5tKtQRPGMA==", + "dependencies": { + "Mono.Cecil": "0.11.6", + "MonoMod.Backports": "1.1.2", + "MonoMod.ILHelpers": "1.1.0" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "StyleCop.Analyzers.Unstable": { + "type": "Transitive", + "resolved": "1.2.0.556", + "contentHash": "zvn9Mqs/ox/83cpYPignI8hJEM2A93s2HkHs8HYMOAQW0PkampyoErAiIyKxgTLqbbad29HX/shv/6LGSjPJNQ==" + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "oTE5IfuMoET8yaZP/vdvy9xO47guAv/rOhe4DODuFBN3ySprcQOlXqO3j+e/H/YpKKR5sglrxRaZ2HYOhNJrqA==" + }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "QhkXUl2gNrQtvPmtBTQHb0YsUrDiDQ2QS09YbtTTiSjGcf7NBqtYbrG/BE06zcBPCKEwQGzIv13IVdXNOSub2w==" + }, + "System.Composition": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "3Djj70fFTraOarSKmRnmRy/zm4YurICm+kiCtI0dYRqGJnLX6nJ+G3WYuFJ173cAPax/gh96REcbNiVqcrypFQ==", + "dependencies": { + "System.Composition.AttributedModel": "9.0.0", + "System.Composition.Convention": "9.0.0", + "System.Composition.Hosting": "9.0.0", + "System.Composition.Runtime": "9.0.0", + "System.Composition.TypedParts": "9.0.0" + } + }, + "System.Composition.AttributedModel": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "iri00l/zIX9g4lHMY+Nz0qV1n40+jFYAmgsaiNn16xvt2RDwlqByNG4wgblagnDYxm3YSQQ0jLlC/7Xlk9CzyA==" + }, + "System.Composition.Convention": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "+vuqVP6xpi582XIjJi6OCsIxuoTZfR0M7WWufk3uGDeCl3wGW6KnpylUJ3iiXdPByPE0vR5TjJgR6hDLez4FQg==", + "dependencies": { + "System.Composition.AttributedModel": "9.0.0" + } + }, + "System.Composition.Hosting": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "OFqSeFeJYr7kHxDfaViGM1ymk7d4JxK//VSoNF9Ux0gpqkLsauDZpu89kTHHNdCWfSljbFcvAafGyBoY094btQ==", + "dependencies": { + "System.Composition.Runtime": "9.0.0" + } + }, + "System.Composition.Runtime": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "w1HOlQY1zsOWYussjFGZCEYF2UZXgvoYnS94NIu2CBnAGMbXFAX8PY8c92KwUItPmowal68jnVLBCzdrWLeEKA==" + }, + "System.Composition.TypedParts": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "aRZlojCCGEHDKqh43jaDgaVpYETsgd7Nx4g1zwLKMtv4iTo0627715ajEFNpEEBTgLmvZuv8K0EVxc3sM4NWJA==", + "dependencies": { + "System.Composition.AttributedModel": "9.0.0", + "System.Composition.Hosting": "9.0.0", + "System.Composition.Runtime": "9.0.0" + } + }, + "System.Configuration.ConfigurationManager": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "PdkuMrwDhXoKFo/JxISIi9E8L+QGn9Iquj2OKDWHB6Y/HnUOuBouF7uS3R4Hw3FoNmwwMo6hWgazQdyHIIs27A==", + "dependencies": { + "System.Diagnostics.EventLog": "9.0.0", + "System.Security.Cryptography.ProtectedData": "9.0.0" + } + }, + "System.Diagnostics.EventLog": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "qd01+AqPhbAG14KtdtIqFk+cxHQFZ/oqRSCoxU1F+Q6Kv0cl726sl7RzU9yLFGd4BUOKdN4XojXF0pQf/R6YeA==" + }, + "System.Formats.Nrbf": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "F/6tNE+ckmdFeSQAyQo26bQOqfPFKEfZcuqnp4kBE6/7jP26diP+QTHCJJ6vpEfaY6bLy+hBLiIQUSxSmNwLkA==" + }, + "System.IO.Pipelines": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "eA3cinogwaNB4jdjQHOP3Z3EuyiDII7MT35jgtnsA4vkn0LUrrSHsU0nzHTzFzmaFYeKV7MYyMxOocFzsBHpTw==" + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "ANiqLu3DxW9kol/hMmTWbt3414t9ftdIuiIU7j80okq2YzAueo120M442xk1kDJWtmZTqWQn7wHDvMRipVOEOQ==" + }, + "System.Reflection.MetadataLoadContext": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "nGdCUVhEQ9/CWYqgaibYEDwIJjokgIinQhCnpmtZfSXdMS6ysLZ8p9xvcJ8VPx6Xpv5OsLIUrho4B9FN+VV/tw==" + }, + "System.Resources.Extensions": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "tvhuT1D2OwPROdL1kRWtaTJliQo0WdyhvwDpd8RM997G7m3Hya5nhbYhNTS75x6Vu+ypSOgL5qxDCn8IROtCxw==", + "dependencies": { + "System.Formats.Nrbf": "9.0.0" + } + }, + "System.Security.Cryptography.Pkcs": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "8tluJF8w9si+2yoHeL8rgVJS6lKvWomTDC8px65Z8MCzzdME5eaPtEQf4OfVGrAxB5fW93ncucy1+221O9EQaw==" + }, + "System.Security.Cryptography.ProtectedData": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "CJW+x/F6fmRQ7N6K8paasTw9PDZp4t7G76UjGNlSDgoHPF0h08vTzLYbLZpOLEJSg35d5wy2jCXGo84EN05DpQ==" + }, + "System.Security.Cryptography.Xml": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "GQZn5wFd+pyOfwWaCbqxG7trQ5ox01oR8kYgWflgtux4HiUNihGEgG2TktRWyH+9bw7NoEju1D41H/upwQeFQw==", + "dependencies": { + "System.Security.Cryptography.Pkcs": "9.0.0" + } + }, + "System.Security.Permissions": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "H2VFD4SFVxieywNxn9/epb63/IOcPPfA0WOtfkljzNfu7GCcHIBQNuwP6zGCEIi7Ci/oj8aLPUNK9sYImMFf4Q==", + "dependencies": { + "System.Windows.Extensions": "9.0.0" + } + }, + "System.Text.Json": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "js7+qAu/9mQvnhA4EfGMZNEzXtJCDxgkgj8ohuxq/Qxv+R56G+ljefhiJHOxTNiw54q8vmABCWUwkMulNdlZ4A==" + }, + "System.Threading.Channels": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "qmeeYNROMsONF6ndEZcIQ+VxR4Q/TX/7uIVLJqtwIWL7dDWeh0l1UIqgo4wYyjG//5lUNhwkLDSFl+pAWO6oiA==" + }, + "System.Threading.Tasks.Dataflow": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "S+y+QuBJNcqOvoFK+rFcZZuQDlD2E4lImKW9/g3E0l7YT2uo4oin9amAn398eGt/xFBYNNSt5O77Dbc38XGfBw==" + }, + "System.Windows.Extensions": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "U9msthvnH2Fsw7xwAvIhNHOdnIjOQTwOc8Vd0oGOsiRcGMGoBFlUD6qtYawRUoQdKH9ysxesZ9juFElt1Jw/7A==" + }, + "TUnit.Assertions": { + "type": "Transitive", + "resolved": "0.75.30", + "contentHash": "mVxJ9U8SlUPcm2/+Z7wiVPxnWFLzZ7ktM8UHZxY4KV0ydxhGSWyrAjOaaifR0N4grPLZJSqniNvG4NV3OMSJ4w==" + }, + "TUnit.Core": { + "type": "Transitive", + "resolved": "0.75.30", + "contentHash": "XRIISUsTTfAvSpD3nOBSCmYJWGl9D21/eDkJl4g/qYBoT9P8KnqDyuQO14UZxMyZR5uTDVC0arnOv8DYb8YQrg==" + }, + "TUnit.Engine": { + "type": "Transitive", + "resolved": "0.75.30", + "contentHash": "0M6OVdfcjjCDmzA8pAzpVgwDcGotun5uMPHK8BsbFGn6Yyf2t7I5Hb1Ke68evOQbNzcQc77yieiRYMzMKAbU8A==", + "dependencies": { + "EnumerableAsyncProcessor": "3.8.4", + "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.0.1", + "Microsoft.Testing.Platform": "2.0.1", + "Microsoft.Testing.Platform.MSBuild": "2.0.1", + "TUnit.Core": "0.75.30" + } + }, + "compatunbreaker": { + "type": "Project", + "dependencies": { + "AsmResolver.DotNet": "[6.0.0-dev, )", + "CompatUnbreaker.Attributes": "[1.0.0-dev, )", + "Meziantou.Analyzer": "[2.0.220, )", + "PolySharp": "[1.15.0, )", + "StyleCop.Analyzers": "[1.2.0-beta.556, )" + } + }, + "compatunbreaker.attributes": { + "type": "Project", + "dependencies": { + "Meziantou.Analyzer": "[2.0.220, )", + "PolySharp": "[1.15.0, )", + "StyleCop.Analyzers": "[1.2.0-beta.556, )" + } + }, + "compatunbreaker.tool": { + "type": "Project", + "dependencies": { + "Basic.Reference.Assemblies.Net90": "[1.8.3, )", + "CompatUnbreaker": "[1.0.0-dev, )", + "Meziantou.Analyzer": "[2.0.220, )", + "Microsoft.Build": "[17.14.28, )", + "Microsoft.Build.Framework": "[17.14.28, )", + "Microsoft.Build.Locator": "[1.10.2, )", + "Microsoft.Build.Tasks.Core": "[17.14.28, )", + "Microsoft.Build.Utilities.Core": "[17.14.28, )", + "Microsoft.CodeAnalysis.CSharp": "[4.14.0, )", + "Microsoft.CodeAnalysis.CSharp.Workspaces": "[4.14.0, )", + "Microsoft.CodeAnalysis.Workspaces.MSBuild": "[4.14.0, )", + "MonoMod.RuntimeDetour": "[25.3.1, )", + "PolySharp": "[1.15.0, )", + "StyleCop.Analyzers": "[1.2.0-beta.556, )" + } + } + } + } +} \ No newline at end of file diff --git a/CompatUnbreaker.Tool/ApiCompatibility/AssemblyMapping/AssemblyMapper.cs b/CompatUnbreaker.Tool/ApiCompatibility/AssemblyMapping/AssemblyMapper.cs new file mode 100644 index 0000000..18136cc --- /dev/null +++ b/CompatUnbreaker.Tool/ApiCompatibility/AssemblyMapping/AssemblyMapper.cs @@ -0,0 +1,64 @@ +using AsmResolver.DotNet; +using CompatUnbreaker.Tool.Utilities.AsmResolver; + +namespace CompatUnbreaker.Tool.ApiCompatibility.AssemblyMapping; + +public sealed class AssemblyMapper(MapperSettings settings) : ElementMapper +{ + private readonly Dictionary _types = new(ExtendedSignatureComparer.VersionAgnostic); + + public IEnumerable Types => _types.Values; + + public override void Add(AssemblyDefinition value, ElementSide side) + { + base.Add(value, side); + + foreach (var module in value.Modules) + { + foreach (var type in module.TopLevelTypes) + { + if (type.Namespace is null || !type.Namespace.Value.StartsWith("System.Threading.Tasks")) continue; + if (settings.Filter(type)) + { + AddOrCreateMapper(type, side); + } + } + + foreach (var exportedType in module.ExportedTypes) + { + var type = exportedType.Resolve(); + if (type == null) + { + Console.WriteLine($"Failed to resolve exported type: {exportedType.FullName}"); + return; + } + + if (settings.Filter(type)) + { + AddOrCreateMapper(type, side); + } + } + } + } + + private void AddOrCreateMapper(TypeDefinition type, ElementSide side) + { + if (!_types.TryGetValue(type, out var mapper)) + { + mapper = new TypeMapper(settings); + _types.Add(type, mapper); + } + + mapper.Add(type, side); + } + + public static AssemblyMapper Create(AssemblyDefinition left, AssemblyDefinition right, MapperSettings? settings = null) + { + var result = new AssemblyMapper(settings ?? new MapperSettings()); + + result.Add(left, ElementSide.Left); + result.Add(right, ElementSide.Right); + + return result; + } +} diff --git a/CompatUnbreaker.Tool/ApiCompatibility/AssemblyMapping/ElementMapper.cs b/CompatUnbreaker.Tool/ApiCompatibility/AssemblyMapping/ElementMapper.cs new file mode 100644 index 0000000..9062c7f --- /dev/null +++ b/CompatUnbreaker.Tool/ApiCompatibility/AssemblyMapping/ElementMapper.cs @@ -0,0 +1,40 @@ +namespace CompatUnbreaker.Tool.ApiCompatibility.AssemblyMapping; + +public abstract class ElementMapper +{ + public T? Left { get; set; } + public T? Right { get; set; } + + public T? this[ElementSide side] => side switch + { + ElementSide.Left => Left, + ElementSide.Right => Right, + _ => throw new ArgumentOutOfRangeException(nameof(side), side, null), + }; + + public virtual void Add(T value, ElementSide side) + { + if (this[side] != null) + { + throw new InvalidOperationException($"{side} element already set."); + } + + switch (side) + { + case ElementSide.Left: + Left = value; + break; + case ElementSide.Right: + Right = value; + break; + default: + throw new ArgumentOutOfRangeException(nameof(side), side, null); + } + } + + public void Deconstruct(out T? left, out T? right) + { + left = Left; + right = Right; + } +} diff --git a/CompatUnbreaker.Tool/ApiCompatibility/AssemblyMapping/ElementSide.cs b/CompatUnbreaker.Tool/ApiCompatibility/AssemblyMapping/ElementSide.cs new file mode 100644 index 0000000..0bb249d --- /dev/null +++ b/CompatUnbreaker.Tool/ApiCompatibility/AssemblyMapping/ElementSide.cs @@ -0,0 +1,7 @@ +namespace CompatUnbreaker.Tool.ApiCompatibility.AssemblyMapping; + +public enum ElementSide : byte +{ + Left = 0, + Right = 1, +} diff --git a/CompatUnbreaker.Tool/ApiCompatibility/AssemblyMapping/MapperSettings.cs b/CompatUnbreaker.Tool/ApiCompatibility/AssemblyMapping/MapperSettings.cs new file mode 100644 index 0000000..0e3c3ee --- /dev/null +++ b/CompatUnbreaker.Tool/ApiCompatibility/AssemblyMapping/MapperSettings.cs @@ -0,0 +1,16 @@ +using AsmResolver.DotNet; +using CompatUnbreaker.Tool.Utilities.AsmResolver; +using CompatUnbreaker.Utilities.AsmResolver; + +namespace CompatUnbreaker.Tool.ApiCompatibility.AssemblyMapping; + +public sealed class MapperSettings +{ + public Func Filter { get; set; } = DefaultFilter; + + private static bool DefaultFilter(IMemberDefinition member) + { + return member.IsVisibleOutsideOfAssembly() && + (member is not TypeDefinition type || type.Namespace != "System.Runtime.CompilerServices"); + } +} diff --git a/CompatUnbreaker.Tool/ApiCompatibility/AssemblyMapping/MemberMapper.cs b/CompatUnbreaker.Tool/ApiCompatibility/AssemblyMapping/MemberMapper.cs new file mode 100644 index 0000000..a9f1b70 --- /dev/null +++ b/CompatUnbreaker.Tool/ApiCompatibility/AssemblyMapping/MemberMapper.cs @@ -0,0 +1,8 @@ +using AsmResolver.DotNet; + +namespace CompatUnbreaker.Tool.ApiCompatibility.AssemblyMapping; + +public sealed class MemberMapper(MapperSettings settings, TypeMapper declaringType) : ElementMapper +{ + public TypeMapper DeclaringType { get; } = declaringType; +} diff --git a/CompatUnbreaker.Tool/ApiCompatibility/AssemblyMapping/TypeMapper.cs b/CompatUnbreaker.Tool/ApiCompatibility/AssemblyMapping/TypeMapper.cs new file mode 100644 index 0000000..b6dd365 --- /dev/null +++ b/CompatUnbreaker.Tool/ApiCompatibility/AssemblyMapping/TypeMapper.cs @@ -0,0 +1,58 @@ +using AsmResolver.DotNet; +using CompatUnbreaker.Tool.Utilities.AsmResolver; + +namespace CompatUnbreaker.Tool.ApiCompatibility.AssemblyMapping; + +public sealed class TypeMapper(MapperSettings settings, TypeMapper? declaringType = null) : ElementMapper +{ + private readonly Dictionary _nestedTypes = new(ExtendedSignatureComparer.VersionAgnostic); + private readonly Dictionary _members = new(ExtendedSignatureComparer.VersionAgnostic); + + public TypeMapper? DeclaringType { get; } = declaringType; + + public IEnumerable NestedTypes => _nestedTypes.Values; + public IEnumerable Members => _members.Values; + + public override void Add(TypeDefinition value, ElementSide side) + { + base.Add(value, side); + + foreach (var member in value.GetMembers(includeNestedTypes: false)) + { + if (settings.Filter(member)) + { + AddOrCreateMapper(member, side); + } + } + + foreach (var nestedType in value.NestedTypes) + { + if (settings.Filter(nestedType)) + { + AddOrCreateMapper(nestedType, side); + } + } + } + + private void AddOrCreateMapper(TypeDefinition nestedType, ElementSide side) + { + if (!_nestedTypes.TryGetValue(nestedType, out var mapper)) + { + mapper = new TypeMapper(settings, this); + _nestedTypes.Add(nestedType, mapper); + } + + mapper.Add(nestedType, side); + } + + private void AddOrCreateMapper(IMemberDefinition member, ElementSide side) + { + if (!_members.TryGetValue(member, out var mapper)) + { + mapper = new MemberMapper(settings, this); + _members.Add(member, mapper); + } + + mapper.Add(member, side); + } +} diff --git a/CompatUnbreaker.Tool/ApiCompatibility/Comparing/ApiComparer.cs b/CompatUnbreaker.Tool/ApiCompatibility/Comparing/ApiComparer.cs new file mode 100644 index 0000000..b07ffb8 --- /dev/null +++ b/CompatUnbreaker.Tool/ApiCompatibility/Comparing/ApiComparer.cs @@ -0,0 +1,81 @@ +using CompatUnbreaker.Tool.ApiCompatibility.AssemblyMapping; +using CompatUnbreaker.Tool.ApiCompatibility.Comparing.Rules; + +namespace CompatUnbreaker.Tool.ApiCompatibility.Comparing; + +public sealed class ApiComparer +{ + private static readonly IEnumerable s_rules = + [ + // new AssemblyIdentityMustMatch(), + new CannotAddAbstractMember(), + new CannotAddMemberToInterface(), + new CannotAddOrRemoveVirtualKeyword(), + new CannotRemoveBaseTypeOrInterface(), + new CannotSealType(), + new EnumsMustMatch(), + new MembersMustExist(), + new CannotChangeVisibility(), + new CannotChangeGenericConstraints(), + ]; + + private readonly List _compatDifferences = []; + + public IEnumerable CompatDifferences => _compatDifferences; + + public void Compare(AssemblyMapper assemblyMapper) + { + AddDifferences(assemblyMapper); + + foreach (var typeMapper in assemblyMapper.Types) + { + Compare(typeMapper); + } + } + + private void Compare(TypeMapper typeMapper) + { + AddDifferences(typeMapper); + + if (typeMapper.Left == null || typeMapper.Right == null) return; + + foreach (var nestedTypeMapper in typeMapper.NestedTypes) + { + Compare(nestedTypeMapper); + } + + foreach (var memberMapper in typeMapper.Members) + { + Compare(memberMapper); + } + } + + private void Compare(MemberMapper memberMapper) + { + AddDifferences(memberMapper); + } + + private void AddDifferences(AssemblyMapper assemblyMapper) + { + foreach (var rule in s_rules) + { + rule.Run(assemblyMapper, _compatDifferences); + } + } + + private void AddDifferences(TypeMapper typeMapper) + { + foreach (var rule in s_rules) + { + rule.Run(typeMapper, _compatDifferences); + } + } + + private void AddDifferences(MemberMapper memberMapper) + { + foreach (var rule in s_rules) + { + rule.Run(memberMapper, _compatDifferences); + } + } +} diff --git a/CompatUnbreaker.Tool/ApiCompatibility/Comparing/CompatDifference.cs b/CompatUnbreaker.Tool/ApiCompatibility/Comparing/CompatDifference.cs new file mode 100644 index 0000000..e7302a3 --- /dev/null +++ b/CompatUnbreaker.Tool/ApiCompatibility/Comparing/CompatDifference.cs @@ -0,0 +1,32 @@ +using AsmResolver.DotNet; +using CompatUnbreaker.Tool.ApiCompatibility.AssemblyMapping; + +namespace CompatUnbreaker.Tool.ApiCompatibility.Comparing; + +public abstract class CompatDifference +{ + public abstract string Message { get; } + public abstract DifferenceType Type { get; } + + public override string ToString() + { + return $"{RemoveSuffix(GetType().Name, "Difference")} : {Message}"; + + static string RemoveSuffix(string str, string suffix) + { + return str.EndsWith(suffix, StringComparison.Ordinal) ? str[..^suffix.Length] : str; + } + } +} + +public abstract class CompatDifference( + TMapper mapper +) : CompatDifference + where TMapper : ElementMapper +{ + public TMapper Mapper { get; } = mapper; +} + +public abstract class TypeCompatDifference(TypeMapper mapper) : CompatDifference(mapper); + +public abstract class MemberCompatDifference(MemberMapper mapper) : CompatDifference(mapper); diff --git a/CompatUnbreaker.Tool/ApiCompatibility/Comparing/DifferenceType.cs b/CompatUnbreaker.Tool/ApiCompatibility/Comparing/DifferenceType.cs new file mode 100644 index 0000000..328c0db --- /dev/null +++ b/CompatUnbreaker.Tool/ApiCompatibility/Comparing/DifferenceType.cs @@ -0,0 +1,8 @@ +namespace CompatUnbreaker.Tool.ApiCompatibility.Comparing; + +public enum DifferenceType +{ + Changed, + Added, + Removed, +} diff --git a/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/BaseRule.cs b/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/BaseRule.cs new file mode 100644 index 0000000..10f5f76 --- /dev/null +++ b/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/BaseRule.cs @@ -0,0 +1,18 @@ +using CompatUnbreaker.Tool.ApiCompatibility.AssemblyMapping; + +namespace CompatUnbreaker.Tool.ApiCompatibility.Comparing.Rules; + +public abstract class BaseRule +{ + public virtual void Run(AssemblyMapper mapper, IList differences) + { + } + + public virtual void Run(TypeMapper mapper, IList differences) + { + } + + public virtual void Run(MemberMapper mapper, IList differences) + { + } +} diff --git a/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotAddAbstractMember.cs b/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotAddAbstractMember.cs new file mode 100644 index 0000000..8c73aee --- /dev/null +++ b/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotAddAbstractMember.cs @@ -0,0 +1,31 @@ +using CompatUnbreaker.Tool.ApiCompatibility.AssemblyMapping; +using CompatUnbreaker.Tool.Utilities.AsmResolver; +using CompatUnbreaker.Utilities.AsmResolver; + +namespace CompatUnbreaker.Tool.ApiCompatibility.Comparing.Rules; + +public sealed class CannotAddAbstractMemberDifference(MemberMapper mapper) : MemberCompatDifference(mapper) +{ + public override string Message => $"Cannot add abstract member '{Mapper.Right}' to {"right"} because it does not exist on {"left"}"; + public override DifferenceType Type => DifferenceType.Added; +} + +public sealed class CannotAddAbstractMember : BaseRule +{ + public override void Run(MemberMapper mapper, IList differences) + { + var (left, right) = mapper; + + if (left == null && right != null && right.IsRoslynAbstract()) + { + // We need to make sure left declaring type is not sealed, as unsealing a type is not a breaking change. + // So if in this version of left and right, right is unsealing the type, abstract members can be added. + // checking for member additions on interfaces is checked on its own rule. + var leftDeclaringType = mapper.DeclaringType.Left; + if (!leftDeclaringType.IsInterface && !leftDeclaringType.IsEffectivelySealed(/* TODO includeInternalSymbols */ false)) + { + differences.Add(new CannotAddAbstractMemberDifference(mapper)); + } + } + } +} diff --git a/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotAddMemberToInterface.cs b/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotAddMemberToInterface.cs new file mode 100644 index 0000000..df5a799 --- /dev/null +++ b/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotAddMemberToInterface.cs @@ -0,0 +1,54 @@ +using AsmResolver.DotNet; +using CompatUnbreaker.Tool.ApiCompatibility.AssemblyMapping; +using CompatUnbreaker.Tool.Utilities.AsmResolver; + +namespace CompatUnbreaker.Tool.ApiCompatibility.Comparing.Rules; + +public sealed class CannotAddMemberToInterfaceDifference(MemberMapper mapper) : MemberCompatDifference(mapper) +{ + public override string Message => $"Cannot add abstract member '{Mapper.Right}' to {"right"} because it does not exist on {"left"}"; + public override DifferenceType Type => DifferenceType.Added; +} + +public sealed class CannotAddMemberToInterface : BaseRule +{ + public override void Run(MemberMapper mapper, IList differences) + { + var (left, right) = mapper; + + if (left == null && right != null && right.DeclaringType.IsInterface) + { + // Fields in interface can only be static which is not considered a break. + if (right is FieldDefinition) + return; + + // Event and property accessors are covered by finding the property or event implementation + // for interface member on the containing type. + if (right is MethodDefinition ms && IsEventOrPropertyAccessor(ms)) + return; + + // If there is a default implementation provided is not a breaking change to add an interface member. + if (right.DeclaringType.FindImplementationForInterfaceMember(right) != null) + return; + + differences.Add(new CannotAddMemberToInterfaceDifference(mapper)); + } + } + + private static bool IsEventOrPropertyAccessor(MethodDefinition symbol) + { + foreach (var property in symbol.DeclaringType.Properties) + { + if (symbol == property.GetMethod || symbol == property.SetMethod) + return true; + } + + foreach (var @event in symbol.DeclaringType.Events) + { + if (symbol == @event.AddMethod || symbol == @event.RemoveMethod) + return true; + } + + return false; + } +} diff --git a/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotAddOrRemoveVirtualKeyword.cs b/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotAddOrRemoveVirtualKeyword.cs new file mode 100644 index 0000000..809f663 --- /dev/null +++ b/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotAddOrRemoveVirtualKeyword.cs @@ -0,0 +1,61 @@ +using AsmResolver.DotNet; +using CompatUnbreaker.Tool.ApiCompatibility.AssemblyMapping; +using CompatUnbreaker.Tool.Utilities.AsmResolver; +using CompatUnbreaker.Utilities.AsmResolver; + +namespace CompatUnbreaker.Tool.ApiCompatibility.Comparing.Rules; + +public sealed class CannotAddSealedToInterfaceMemberDifference(MemberMapper mapper) : MemberCompatDifference(mapper) +{ + public override string Message => $"Cannot add sealed keyword to default interface member '{Mapper.Right}'"; + public override DifferenceType Type => DifferenceType.Added; +} + +public sealed class CannotRemoveVirtualFromMemberDifference(MemberMapper mapper) : MemberCompatDifference(mapper) +{ + public override string Message => $"Cannot remove virtual keyword from member '{Mapper.Right}'"; + public override DifferenceType Type => DifferenceType.Removed; +} + +public sealed class CannotAddOrRemoveVirtualKeyword : BaseRule +{ + private static bool IsSealed(IMemberDefinition member) => member.IsRoslynSealed() || (!member.IsRoslynVirtual() && !member.IsRoslynAbstract()); + + public override void Run(MemberMapper mapper, IList differences) + { + var (left, right) = mapper; + + // Members must exist + if (left is null || right is null) + { + return; + } + + if (left.DeclaringType.IsInterface || right.DeclaringType.IsInterface) + { + if (!IsSealed(left) && IsSealed(right)) + { + // Introducing the sealed keyword to an interface method is a breaking change. + differences.Add(new CannotAddSealedToInterfaceMemberDifference(mapper)); + } + + return; + } + + if (left.IsRoslynVirtual()) + { + // Removing the virtual keyword from a member in a sealed type won't be a breaking change. + if (left.DeclaringType.IsEffectivelySealed( /* TODO includeInternalSymbols */ false)) + { + return; + } + + // If left is virtual and right is not, then emit a diagnostic + // specifying that the virtual modifier cannot be removed. + if (!right.IsRoslynVirtual()) + { + differences.Add(new CannotRemoveVirtualFromMemberDifference(mapper)); + } + } + } +} diff --git a/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotChangeGenericConstraints.cs b/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotChangeGenericConstraints.cs new file mode 100644 index 0000000..5273b6b --- /dev/null +++ b/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotChangeGenericConstraints.cs @@ -0,0 +1,130 @@ +using System.Diagnostics; +using AsmResolver.DotNet; +using CompatUnbreaker.Tool.ApiCompatibility.AssemblyMapping; +using CompatUnbreaker.Tool.Utilities.AsmResolver; + +namespace CompatUnbreaker.Tool.ApiCompatibility.Comparing.Rules; + +public sealed class CannotChangeGenericConstraintDifference(DifferenceType type, IMemberDefinition left, IMemberDefinition right, GenericParameter leftTypeParameter, string constraint) : CompatDifference +{ + public override string Message => $"Cannot {(type == DifferenceType.Added ? "add" : "remove")} constraint '{constraint}' on type parameter '{leftTypeParameter}' of '{left}'"; + public override DifferenceType Type => type; +} + +public sealed class CannotChangeGenericConstraints : BaseRule +{ + public override void Run(TypeMapper mapper, IList differences) + { + var (left, right) = mapper; + if (left == null || right == null) + return; + + var leftTypeParameters = left.GenericParameters; + var rightTypeParameters = right.GenericParameters; + + // can remove constraints on sealed classes since no code should observe broader set of type parameters + var permitConstraintRemoval = left.IsSealed; + + CompareTypeParameters(leftTypeParameters, rightTypeParameters, left, right, permitConstraintRemoval, differences); + } + + public override void Run(MemberMapper mapper, IList differences) + { + var (left, right) = mapper; + if (left is not MethodDefinition leftMethod || right is not MethodDefinition rightMethod) + { + return; + } + + var leftTypeParameters = leftMethod.GenericParameters; + var rightTypeParameters = rightMethod.GenericParameters; + + var permitConstraintRemoval = !leftMethod.IsVirtual; + + CompareTypeParameters(leftTypeParameters, rightTypeParameters, left, right, permitConstraintRemoval, differences); + } + + private void CompareTypeParameters( + IList leftTypeParameters, + IList rightTypeParameters, + IMemberDefinition left, + IMemberDefinition right, + bool permitConstraintRemoval, + IList differences + ) + { + Debug.Assert(leftTypeParameters.Count == rightTypeParameters.Count); + for (var i = 0; i < leftTypeParameters.Count; i++) + { + var leftTypeParam = leftTypeParameters[i]; + var rightTypeParam = rightTypeParameters[i]; + + var addedConstraints = new List(); + var removedConstraints = new List(); + + // CompareBoolConstraint(typeParam => typeParam.HasConstructorConstraint, "new()"); TODO + // CompareBoolConstraint(typeParam => typeParam.HasNotNullConstraint, "notnull"); TODO + CompareBoolConstraint(typeParam => typeParam.HasReferenceTypeConstraint, "class"); + // CompareBoolConstraint(typeParam => typeParam.HasUnmanagedTypeConstraint, "unmanaged"); TODO + // unmanaged implies struct + // CompareBoolConstraint(typeParam => typeParam.HasValueTypeConstraint & !typeParam.HasUnmanagedTypeConstraint, "struct"); TODO + + var rightOnlyConstraints = ToHashSet(rightTypeParam.Constraints); + rightOnlyConstraints.ExceptWith(ToHashSet(leftTypeParam.Constraints)); + + // we could allow an addition if removals are allowed, and the addition is a less-derived base type or interface + // for example: changing a constraint from MemoryStream to Stream on a sealed type, or non-virtual member + // but we'll leave this to suppressions + + addedConstraints.AddRange(rightOnlyConstraints.Select(x => x.FullName)); + + // additions + foreach (var addedConstraint in addedConstraints) + { + differences.Add(new CannotChangeGenericConstraintDifference(DifferenceType.Added, left, right, leftTypeParam, addedConstraint)); + } + + // removals + // we could allow a removal in the case of reducing to more-derived interfaces if those interfaces were previous constraints + // for example if IB : IA and a type is constrained by both IA and IB, it's safe to remove IA since it's implied by IB + // but we'll leave this to suppressions + + if (!permitConstraintRemoval) + { + var leftOnlyConstraints = ToHashSet(leftTypeParam.Constraints); + leftOnlyConstraints.ExceptWith(ToHashSet(rightTypeParam.Constraints)); + removedConstraints.AddRange(leftOnlyConstraints.Select(x => x.FullName)); + + foreach (var removedConstraint in removedConstraints) + { + differences.Add(new CannotChangeGenericConstraintDifference(DifferenceType.Removed, left, right, leftTypeParam, removedConstraint)); + } + } + + void CompareBoolConstraint(Func boolConstraint, string constraintName) + { + var leftBoolConstraint = boolConstraint(leftTypeParam); + var rightBoolConstraint = boolConstraint(rightTypeParam); + + // addition + if (!leftBoolConstraint && rightBoolConstraint) + { + addedConstraints.Add(constraintName); + } + // removal + else if (!permitConstraintRemoval && leftBoolConstraint && !rightBoolConstraint) + { + removedConstraints.Add(constraintName); + } + } + } + } + + private static HashSet ToHashSet(IList constraints) + { + return constraints + .Select(c => c.Constraint) + .Where(c => c != null) + .ToHashSet(ExtendedSignatureComparer.VersionAgnostic!); + } +} diff --git a/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotChangeVisibility.cs b/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotChangeVisibility.cs new file mode 100644 index 0000000..11b2ddc --- /dev/null +++ b/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotChangeVisibility.cs @@ -0,0 +1,79 @@ +using AsmResolver.DotNet; +using CompatUnbreaker.Tool.ApiCompatibility.AssemblyMapping; +using CompatUnbreaker.Tool.Utilities.AsmResolver; +using CompatUnbreaker.Utilities.AsmResolver; +using Microsoft.CodeAnalysis; +using Accessibility = CompatUnbreaker.Utilities.AsmResolver.Accessibility; + +namespace CompatUnbreaker.Tool.ApiCompatibility.Comparing.Rules; + +public sealed class CannotReduceVisibilityDifference(IMemberDefinition left, IMemberDefinition right) : CompatDifference +{ + public override string Message => $"Visibility of '{left}' reduced from '{left.GetAccessibility()}' to '{right.GetAccessibility()}'"; + public override DifferenceType Type => DifferenceType.Changed; +} + +public sealed class CannotChangeVisibility : BaseRule +{ + public override void Run(TypeMapper mapper, IList differences) + { + Run(mapper.Left, mapper.Right, differences); + } + + public override void Run(MemberMapper mapper, IList differences) + { + Run(mapper.Left, mapper.Right, differences); + } + + private static Accessibility NormalizeInternals(Accessibility a) => a switch + { + Accessibility.ProtectedOrInternal => Accessibility.Protected, + Accessibility.ProtectedAndInternal or Accessibility.Internal => Accessibility.Private, + _ => a, + }; + + private int CompareAccessibility(Accessibility a, Accessibility b) + { + // if (!_settings.IncludeInternalSymbols) TODO + { + a = NormalizeInternals(a); + b = NormalizeInternals(b); + } + + if (a == b) + { + return 0; + } + + return (a, b) switch + { + (Accessibility.Public, _) => 1, + (_, Accessibility.Public) => -1, + (Accessibility.ProtectedOrInternal, _) => 1, + (_, Accessibility.ProtectedOrInternal) => -1, + (Accessibility.Protected or Accessibility.Internal, _) => 1, + (_, Accessibility.Protected or Accessibility.Internal) => -1, + (Accessibility.ProtectedAndInternal, _) => 1, + (_, Accessibility.ProtectedAndInternal) => -1, + _ => throw new NotImplementedException(), + }; + } + + private void Run(IMemberDefinition? left, IMemberDefinition? right, IList differences) + { + // The MemberMustExist rule handles missing symbols and therefore this rule only runs when left and right is not null. + if (left is null || right is null) + { + return; + } + + var leftAccess = left.GetAccessibility(); + var rightAccess = right.GetAccessibility(); + int accessComparison = CompareAccessibility(leftAccess, rightAccess); + + if (accessComparison > 0) + { + differences.Add(new CannotReduceVisibilityDifference(left, right)); + } + } +} diff --git a/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotRemoveBaseTypeOrInterface.cs b/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotRemoveBaseTypeOrInterface.cs new file mode 100644 index 0000000..143c3e9 --- /dev/null +++ b/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotRemoveBaseTypeOrInterface.cs @@ -0,0 +1,92 @@ +using AsmResolver.DotNet; +using CompatUnbreaker.Tool.ApiCompatibility.AssemblyMapping; +using CompatUnbreaker.Tool.Utilities.AsmResolver; +using CompatUnbreaker.Utilities.AsmResolver; + +namespace CompatUnbreaker.Tool.ApiCompatibility.Comparing.Rules; + +public sealed class CannotRemoveBaseTypeDifference(TypeMapper mapper) : TypeCompatDifference(mapper) +{ + public override string Message => $"Type '{Mapper.Left}' does not inherit from base type '{Mapper.Left.BaseType}' on {"right"} but it does on {"left"}"; + + public override DifferenceType Type => DifferenceType.Changed; +} + +public sealed class CannotRemoveBaseInterfaceDifference(TypeMapper mapper, TypeDefinition leftInterface) : TypeCompatDifference(mapper) +{ + public override string Message => $"Type '{Mapper.Left}' does not implement interface '{leftInterface}' on {"right"} but it does on {"left"}"; + + public override DifferenceType Type => DifferenceType.Changed; +} + +public sealed class CannotRemoveBaseTypeOrInterface : BaseRule +{ + public override void Run(TypeMapper mapper, IList differences) + { + var (left, right) = mapper; + + if (left == null || right == null) + return; + + if (!left.IsInterface && !right.IsInterface) + { + // if left and right are not interfaces check base types + ValidateBaseTypeNotRemoved(mapper, differences); + } + + ValidateInterfaceNotRemoved(mapper, differences); + } + + private void ValidateBaseTypeNotRemoved(TypeMapper mapper, IList differences) + { + var (left, right) = mapper; + + if (left == null || right == null) + return; + + var leftBaseType = left.BaseType; + var rightBaseType = right.BaseType; + + if (leftBaseType == null) + return; + + while (rightBaseType != null) + { + // If we found the immediate left base type on right we can assume + // that any removal of a base type up on the hierarchy will be handled + // when validating the type which it's base type was actually removed. + if (ExtendedSignatureComparer.VersionAgnostic.Equals(leftBaseType, rightBaseType)) + return; + + rightBaseType = rightBaseType.Resolve().BaseType; + } + + differences.Add(new CannotRemoveBaseTypeDifference(mapper)); + } + + private void ValidateInterfaceNotRemoved(TypeMapper mapper, IList differences) + { + var (left, right) = mapper; + + if (left == null || right == null) + return; + + var rightInterfaces = new HashSet(right.GetAllBaseInterfaces(), ExtendedSignatureComparer.VersionAgnostic); + + foreach (var leftInterface in left.GetAllBaseInterfaces()) + { + // Ignore non visible interfaces based on the run Settings + // If TypeKind == Error it means the Roslyn couldn't resolve it, + // so we are running with a missing assembly reference to where that type ef is defined. + // However we still want to consider it as Roslyn does resolve it's name correctly. + if (!leftInterface.IsVisibleOutsideOfAssembly( /* TODO IncludeInternalSymbols */ false)) + return; + + if (!rightInterfaces.Contains(leftInterface)) + { + differences.Add(new CannotRemoveBaseInterfaceDifference(mapper, leftInterface)); + return; + } + } + } +} diff --git a/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotSealType.cs b/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotSealType.cs new file mode 100644 index 0000000..5fd8fb1 --- /dev/null +++ b/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotSealType.cs @@ -0,0 +1,33 @@ +using CompatUnbreaker.Tool.ApiCompatibility.AssemblyMapping; +using CompatUnbreaker.Tool.Utilities.AsmResolver; +using CompatUnbreaker.Utilities.AsmResolver; + +namespace CompatUnbreaker.Tool.ApiCompatibility.Comparing.Rules; + +public sealed class CannotSealTypeDifference(TypeMapper mapper) : TypeCompatDifference(mapper) +{ + public override string Message => Mapper.Right.IsSealed + ? $"Type '{Mapper.Right}' has the sealed modifier on {"right"} but not on {"left"}" + : $"Type '{Mapper.Right}' is sealed because it has no visible constructor on {"right"} but it does on {"left"}"; + + public override DifferenceType Type => DifferenceType.Changed; +} + +public sealed class CannotSealType : BaseRule +{ + public override void Run(TypeMapper mapper, IList differences) + { + var (left, right) = mapper; + + if (left == null || right == null || left.IsInterface || right.IsInterface) + return; + + var isLeftSealed = left.IsEffectivelySealed( /* TODO includeInternalSymbols */ false); + var isRightSealed = right.IsEffectivelySealed( /* TODO includeInternalSymbols */ false); + + if (!isLeftSealed && isRightSealed) + { + differences.Add(new CannotSealTypeDifference(mapper)); + } + } +} diff --git a/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/EnumsMustMatch.cs b/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/EnumsMustMatch.cs new file mode 100644 index 0000000..66bf20f --- /dev/null +++ b/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/EnumsMustMatch.cs @@ -0,0 +1,76 @@ +using AsmResolver.DotNet; +using CompatUnbreaker.Tool.ApiCompatibility.AssemblyMapping; +using CompatUnbreaker.Tool.Utilities.AsmResolver; + +namespace CompatUnbreaker.Tool.ApiCompatibility.Comparing.Rules; + +public sealed class EnumTypesMustMatch(TypeMapper mapper) : TypeCompatDifference(mapper) +{ + public override string Message => $"Underlying type of enum '{Mapper.Left}' changed from '{Mapper.Left.GetEnumUnderlyingType()}' to '{Mapper.Right.GetEnumUnderlyingType()}'"; + + public override DifferenceType Type => DifferenceType.Changed; +} + +public sealed class EnumValuesMustMatchDifference(TypeMapper mapper, FieldDefinition leftField, FieldDefinition rightField) : TypeCompatDifference(mapper) +{ + public override string Message => $"Value of field '{Mapper.Left}' in enum '{leftField.Name}' changed from '{leftField.Constant?.InterpretData()}' to '{rightField.Constant?.InterpretData()}'"; + + public override DifferenceType Type => DifferenceType.Changed; +} + +public sealed class EnumsMustMatch : BaseRule +{ + public override void Run(TypeMapper mapper, IList differences) + { + var (left, right) = mapper; + + // Ensure that this rule only runs on enums. + if (left == null || right == null || !left.IsEnum || !right.IsEnum) + return; + + // Get enum's underlying type. + if (left.GetEnumUnderlyingType() is not { } leftType || right.GetEnumUnderlyingType() is not { } rightType) + { + return; + } + + // Check that the underlying types are equal and if not, emit a diagnostic. + if (!ExtendedSignatureComparer.VersionAgnostic.Equals(leftType, rightType)) + { + differences.Add(new EnumTypesMustMatch(mapper)); + return; + } + + // If so, compare their fields. + // Build a map of the enum's fields, keyed by the field names. + var leftMembers = left.Fields + .Where(f => f.IsStatic) + .ToDictionary(a => a.Name!.Value); + var rightMembers = right.Fields + .Where(f => f.IsStatic) + .ToDictionary(a => a.Name!.Value); + + // For each field that is present in the left and right, check that their constant values match. + // Otherwise, emit a diagnostic. + foreach (var lEntry in leftMembers) + { + if (!rightMembers.TryGetValue(lEntry.Key, out var rField)) + { + continue; + } + + if (lEntry.Value.Constant is not { } leftConstant || rField.Constant is not { } rightConstant || !Equals(leftConstant, rightConstant)) + { + differences.Add(new EnumValuesMustMatchDifference(mapper, lEntry.Value, rField)); + } + } + } + + private static bool Equals(Constant left, Constant right) + { + if (left.Type != right.Type) + return false; + + return Equals(left.InterpretData(), right.InterpretData()); + } +} diff --git a/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/MembersMustExist.cs b/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/MembersMustExist.cs new file mode 100644 index 0000000..8535762 --- /dev/null +++ b/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/MembersMustExist.cs @@ -0,0 +1,91 @@ +using AsmResolver.DotNet; +using CompatUnbreaker.Tool.ApiCompatibility.AssemblyMapping; +using CompatUnbreaker.Tool.Utilities.AsmResolver; + +namespace CompatUnbreaker.Tool.ApiCompatibility.Comparing.Rules; + +public sealed class TypeMustExistDifference(TypeMapper mapper) : TypeCompatDifference(mapper) +{ + public override string Message => $"Type '{Mapper.Left}' exists on {"left"} but not on {"right"}"; + public override DifferenceType Type => DifferenceType.Removed; +} + +public sealed class MemberMustExistDifference(MemberMapper mapper) : MemberCompatDifference(mapper) +{ + public override string Message => $"Member '{Mapper.Left}' exists on {"left"} but not on {"right"}"; + public override DifferenceType Type => DifferenceType.Removed; +} + +public sealed class MembersMustExist : BaseRule +{ + public override void Run(TypeMapper mapper, IList differences) + { + var (left, right) = mapper; + + if (left != null && right == null) + { + differences.Add(new TypeMustExistDifference(mapper)); + } + } + + public override void Run(MemberMapper mapper, IList differences) + { + var (left, right) = mapper; + + if (left != null && right == null) + { + if (ShouldReportMissingMember(left, mapper.DeclaringType.Right)) + { + differences.Add(new MemberMustExistDifference(mapper)); + } + } + } + + private static bool ShouldReportMissingMember(IMemberDefinition member, TypeDefinition? declaringType) + { + // TODO make this an option I guess + // // Events and properties are handled via their accessors. + // if (member is PropertyDefinition or EventDefinition) + // return false; + + if (member is MethodDefinition method) + { + // Will be handled by a different rule + if (method.IsExplicitInterfaceImplementation()) + return false; + + // If method is an override or is promoted to the base type should not be reported. + if (method.IsOverride() || FindMatchingOnBaseType(method, declaringType)) + return false; + } + + return true; + } + + private static bool FindMatchingOnBaseType(MethodDefinition method, TypeDefinition? declaringType) + { + // Constructors cannot be promoted + if (method.IsConstructor) + return false; + + if (declaringType != null) + { + foreach (var type in declaringType.GetAllBaseTypes()) + { + foreach (var candidate in type.Methods) + { + if (IsMatchingMethod(method, candidate)) + return true; + } + } + } + + return false; + } + + private static bool IsMatchingMethod(MethodDefinition method, MethodDefinition candidate) + { + return method.Name == candidate.Name && + ExtendedSignatureComparer.VersionAgnostic.Equals(method.Signature, candidate.Signature); + } +} diff --git a/CompatUnbreaker.Tool/ApiCompatibility/README.md b/CompatUnbreaker.Tool/ApiCompatibility/README.md new file mode 100644 index 0000000..f8e62d0 --- /dev/null +++ b/CompatUnbreaker.Tool/ApiCompatibility/README.md @@ -0,0 +1 @@ +Heavily based on [Microsoft.DotNet.ApiCompatibility](https://github.com/dotnet/sdk/tree/main/src/Compatibility/ApiCompat/Microsoft.DotNet.ApiCompatibility), but with AsmResolver instead of Roslyn diff --git a/CompatUnbreaker.Tool/Commands/BaseShimProjectCommand.cs b/CompatUnbreaker.Tool/Commands/BaseShimProjectCommand.cs new file mode 100644 index 0000000..2beb9fa --- /dev/null +++ b/CompatUnbreaker.Tool/Commands/BaseShimProjectCommand.cs @@ -0,0 +1,112 @@ +using AsmResolver.DotNet; +using CompatUnbreaker.Models; +using CompatUnbreaker.Processors; +using CompatUnbreaker.Processors.Abstractions; +using CompatUnbreaker.Tool.ApiCompatibility.AssemblyMapping; +using CompatUnbreaker.Tool.ApiCompatibility.Comparing; +using CompatUnbreaker.Tool.SkeletonGeneration; +using CompatUnbreaker.Utilities.AsmResolver; +using ConsoleAppFramework; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using Microsoft.Build.Locator; +using Microsoft.Build.Logging; + +namespace CompatUnbreaker.Tool.Commands; + +public abstract class BaseShimProjectCommand +{ + static BaseShimProjectCommand() + { + MSBuildLocator.RegisterDefaults(); + } + + private static (List FrameworkReferences, List References) BuildAndGetReferences(ref ProjectInstance project, bool build) + { + var loggers = new ConsoleLogger[] + { + new ConsoleLogger(LoggerVerbosity.Quiet), + }; + + var targetFrameworks = project.GetPropertyValue("TargetFrameworks"); + if (!string.IsNullOrEmpty(targetFrameworks)) + { + project = new ProjectInstance(project.FullPath, new Dictionary + { + ["TargetFramework"] = targetFrameworks.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)[0], + }, null); + } + + if (!build) + { + project.SetProperty("BuildProjectReferences", "false"); + } + + if (!project.Build("Restore", loggers)) + { + throw new Exception("Failed to restore packages"); + } + + if (!project.Build(build ? "Build" : "FindReferenceAssembliesForReferences", loggers)) + { + throw new Exception("Failed to build"); + } + + var frameworkReferences = new List(); + var references = new List(); + + foreach (var reference in project.GetItems("ReferencePathWithRefAssemblies")) + { + if (reference.HasMetadata("FrameworkReferenceName") || reference.GetMetadataValue("NuGetPackageId") == "NETStandard.Library") + { + frameworkReferences.Add(reference.EvaluatedInclude); + } + else + { + references.Add(reference.EvaluatedInclude); + } + } + + return (frameworkReferences, references); + } + + protected async Task LoadAsync([Argument] string projectPath, bool skipBuild = false) + { + var shimProject = new ProjectInstance(projectPath); + + var shimTargetAssemblyName = shimProject.GetPropertyValue("ShimTargetAssemblyName"); + var shimBaselineProjectPath = shimProject.GetPropertyValue("ShimBaselineProject"); + + var shimBaselineProject = new ProjectInstance(Path.Combine(shimProject.Directory, shimBaselineProjectPath)); + + var (shimFrameworkReferences, shimReferences) = BuildAndGetReferences(ref shimProject, !skipBuild); + var (baselineFrameworkReferences, baselineReferences) = BuildAndGetReferences(ref shimBaselineProject, false); + + var shimPath = shimProject.GetPropertyValue("TargetPath"); + + baselineReferences.RemoveAll(r => shimFrameworkReferences.Any(r2 => Path.GetFileName(r2) == Path.GetFileName(r))); + + var shimReferenceAssemblies = shimFrameworkReferences.Concat(shimReferences).Select(AssemblyDefinition.FromFile).ToArray(); + var baselineReferenceAssemblies = shimFrameworkReferences.Concat(baselineReferences).Select(AssemblyDefinition.FromFile).ToArray(); + + var shimAssembly = new SimpleAssemblyResolver(shimReferenceAssemblies).Load(shimPath); + var targetAssembly = shimReferenceAssemblies.Single(a => a.Name == shimTargetAssemblyName); + + var baselineResolver = new SimpleAssemblyResolver(baselineReferenceAssemblies); + var baselineAssembly = baselineReferenceAssemblies.Single(a => a.Name == shimTargetAssemblyName); + + foreach (var assemblyReference in baselineAssembly.Modules.Concat(targetAssembly.Modules).SelectMany(m => m.AssemblyReferences)) + { + if (assemblyReference.Resolve() == null) + { + throw new InvalidOperationException($"Couldn't resolve {assemblyReference}"); + } + } + + Unbreaker.ProcessReference(shimAssembly, targetAssembly); + + var assemblyMapper = AssemblyMapper.Create(baselineAssembly, targetAssembly); + + return assemblyMapper; + } +} diff --git a/CompatUnbreaker.Tool/Commands/CompareCommand.cs b/CompatUnbreaker.Tool/Commands/CompareCommand.cs new file mode 100644 index 0000000..9dfc662 --- /dev/null +++ b/CompatUnbreaker.Tool/Commands/CompareCommand.cs @@ -0,0 +1,21 @@ +using CompatUnbreaker.Tool.ApiCompatibility.Comparing; +using ConsoleAppFramework; + +namespace CompatUnbreaker.Tool.Commands; + +internal sealed class CompareCommand : BaseShimProjectCommand +{ + [Command("compare")] + public async Task ExecuteAsync([Argument] string projectPath, bool skipBuild = false) + { + var assemblyMapper = await LoadAsync(projectPath, skipBuild); + + var apiComparer = new ApiComparer(); + apiComparer.Compare(assemblyMapper); + + foreach (var difference in apiComparer.CompatDifferences) + { + Console.WriteLine(difference); + } + } +} diff --git a/CompatUnbreaker.Tool/Commands/SkeletonCommand.cs b/CompatUnbreaker.Tool/Commands/SkeletonCommand.cs new file mode 100644 index 0000000..977f940 --- /dev/null +++ b/CompatUnbreaker.Tool/Commands/SkeletonCommand.cs @@ -0,0 +1,15 @@ +using CompatUnbreaker.Tool.SkeletonGeneration; +using ConsoleAppFramework; + +namespace CompatUnbreaker.Tool.Commands; + +internal sealed class SkeletonCommand : BaseShimProjectCommand +{ + [Command("skeleton")] + public async Task ExecuteAsync([Argument] string projectPath, bool skipBuild = false) + { + var assemblyMapper = await LoadAsync(projectPath, skipBuild); + + await SkeletonGenerator.GenerateAsync(assemblyMapper, projectPath); + } +} diff --git a/CompatUnbreaker.Tool/CompatUnbreaker.Tool.csproj b/CompatUnbreaker.Tool/CompatUnbreaker.Tool.csproj new file mode 100644 index 0000000..b145b28 --- /dev/null +++ b/CompatUnbreaker.Tool/CompatUnbreaker.Tool.csproj @@ -0,0 +1,35 @@ + + + net$(NETCoreAppMaximumVersion) + Exe + + + + + + + + + + + + + + + + + + + + + + + + + + + + false + + + diff --git a/CompatUnbreaker.Tool/Program.cs b/CompatUnbreaker.Tool/Program.cs new file mode 100644 index 0000000..5e8d32e --- /dev/null +++ b/CompatUnbreaker.Tool/Program.cs @@ -0,0 +1,17 @@ +using CompatUnbreaker.Tool.Commands; +using ConsoleAppFramework; + +namespace CompatUnbreaker.Tool; + +internal sealed class Program +{ + public static async Task Main(string[] args) + { + var app = ConsoleApp.Create(); + + app.Add(); + app.Add(); + + await app.RunAsync(args); + } +} diff --git a/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/AsmResolverTypeSyntaxGenerator.cs b/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/AsmResolverTypeSyntaxGenerator.cs new file mode 100644 index 0000000..80ac0cd --- /dev/null +++ b/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/AsmResolverTypeSyntaxGenerator.cs @@ -0,0 +1,377 @@ +using System.Diagnostics; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using CompatUnbreaker.Tool.Utilities; +using CompatUnbreaker.Tool.Utilities.AsmResolver; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Simplification; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace CompatUnbreaker.Tool.SkeletonGeneration.CodeGeneration; + +internal sealed class AsmResolverTypeSyntaxGenerator : ITypeSignatureVisitor +{ + private static AsmResolverTypeSyntaxGenerator Instance { get; } = new(); + + private static TTypeSyntax AddInformationTo(TTypeSyntax syntax, ITypeDescriptor symbol) + where TTypeSyntax : TypeSyntax + { + // syntax = syntax.WithPrependedLeadingTrivia(ElasticMarker).WithAppendedTrailingTrivia(ElasticMarker); + // syntax = syntax.WithAdditionalAnnotations(SymbolAnnotation.Create(symbol)); + + return syntax; + } + + public static TypeSyntax TypeExpression(ITypeDescriptor type, TypeContext context) + { + return Instance.VisitTypeDescriptor(type, context); + } + + private TypeSyntax VisitTypeDescriptor(ITypeDescriptor type, TypeContext context) => type switch + { + TypeSignature signature => signature.AcceptVisitor(this, context), + ITypeDefOrRef reference => VisitTypeDefOrRef(reference, context), + ExportedType exported => VisitSimpleType(exported, context), + _ => throw new ArgumentOutOfRangeException(nameof(type)), + }; + + private TypeSyntax VisitTypeDefOrRef(ITypeDefOrRef type, TypeContext context) => type switch + { + TypeSpecification specification => VisitTypeDescriptor(specification.Signature, context), + _ => VisitSimpleType(type, context), + }; + + private TypeSyntax VisitSimpleType(ITypeDescriptor symbol, TypeContext context, IList? typeArguments = null) + { + var name = MetadataHelpers.InferTypeArityAndUnmangleMetadataName(symbol.Name, out var arity).ToString(); + + var isNonGenericValueType = arity > 0 + ? symbol.IsTypeOf("System", "Nullable`1") + : symbol.IsValueType; + + var nullableAnnotation = isNonGenericValueType + ? NullableAnnotation.Oblivious + : context.Transform.TryConsumeNullableTransform() ?? NullableAnnotation.Oblivious; + + if (context.Transform.TryConsumeDynamicTransform() == true) + { + return IdentifierName("dynamic"); + } + + if (typeArguments != null && IsTupleTypeOfCardinality(new GenericInstanceTypeSignature(symbol.ToTypeDefOrRef(), true, typeArguments.ToArray()), out var cardinality)) + { + var names = context.Transform.ConsumeTupleElementNames(cardinality); + + var elements = default(SeparatedSyntaxList); + + for (var i = 0; i < Math.Min(typeArguments.Count, ValueTupleRestPosition - 1); i++) + { + var elementTypeSignature = typeArguments[i]; + var elementType = elementTypeSignature.AcceptVisitor(this, context); + + elements = elements.Add( + names.IsEmpty + ? TupleElement(elementType) + : TupleElement(elementType, names[i]?.ToIdentifierToken() ?? default) + ); + } + + if (typeArguments.Count >= ValueTupleRestPosition) + { + var rest = (TupleTypeSyntax) typeArguments[ValueTupleRestPosition - 1].AcceptVisitor(this, context); + + for (var i = 0; i < rest.Elements.Count; i++) + { + var element = rest.Elements[i]; + + elements = elements.Add( + names.IsEmpty + ? element + : element.WithIdentifier(names[ValueTupleRestPosition - 1 + i]?.ToIdentifierToken() ?? default) + ); + } + } + + return TupleType(elements); + } + + SimpleNameSyntax simpleNameSyntax; + + if (arity == 0) + { + simpleNameSyntax = name.ToIdentifierName(); + } + else + { + simpleNameSyntax = GenericName( + name.ToIdentifierToken(), + TypeArgumentList([ + .. typeArguments == null + ? Enumerable.Repeat(OmittedTypeArgument(), arity) + : typeArguments.Select(t => VisitTypeDescriptor(t, context)), + ]) + ); + } + + TypeSyntax typeSyntax; + + if (symbol.DeclaringType != null) + { + var declaringTypeSyntax = VisitTypeDescriptor(symbol.DeclaringType, context); + if (declaringTypeSyntax is NameSyntax declaringTypeName) + { + typeSyntax = QualifiedName(declaringTypeName, simpleNameSyntax); + } + else + { + typeSyntax = simpleNameSyntax; + } + + typeSyntax = AddInformationTo(typeSyntax, symbol); + } + else + { + typeSyntax = AddInformationTo(AddNamespace(symbol.Namespace, simpleNameSyntax), symbol); + } + + if (nullableAnnotation == NullableAnnotation.Annotated) + { + typeSyntax = AddInformationTo(NullableType(typeSyntax), symbol); + } + + return typeSyntax; + } + + private const int ValueTupleRestPosition = 8; + + private static bool IsTupleTypeOfCardinality(TypeSignature signature, out int tupleCardinality) + { + if (signature is GenericInstanceTypeSignature genericSignature && + genericSignature.Namespace == "System" && + genericSignature.GetUnmangledName() is "ValueTuple") + { + var arity = genericSignature.TypeArguments.Count; + + if (arity is >= 0 and < ValueTupleRestPosition) + { + tupleCardinality = arity; + return true; + } + + if (arity == ValueTupleRestPosition) + { + var typeToCheck = genericSignature; + var levelsOfNesting = 0; + + do + { + levelsOfNesting++; + typeToCheck = (GenericInstanceTypeSignature) typeToCheck.TypeArguments[ValueTupleRestPosition - 1]; + } while (SignatureComparer.Default.Equals(typeToCheck.GenericType, genericSignature.GenericType)); + + arity = typeToCheck.TypeArguments.Count; + + if (arity is > 0 and < ValueTupleRestPosition && IsTupleTypeOfCardinality(typeToCheck, out tupleCardinality)) + { + Debug.Assert(tupleCardinality < ValueTupleRestPosition); + tupleCardinality += (ValueTupleRestPosition - 1) * levelsOfNesting; + return true; + } + } + } + + tupleCardinality = 0; + return false; + } + + private static NameSyntax AddNamespace(string? @namespace, SimpleNameSyntax name) + { + if (string.IsNullOrEmpty(@namespace)) + { + return AddGlobalAlias(name); + } + + var parts = @namespace.Split('.'); + NameSyntax namespaceName = parts[0].ToIdentifierName(); + for (var i = 1; i < parts.Length; i++) + { + namespaceName = QualifiedName(namespaceName, parts[i].ToIdentifierName()); + } + + return QualifiedName(ParseName(@namespace), name); + } + + private static AliasQualifiedNameSyntax AddGlobalAlias(SimpleNameSyntax syntax) + { + return AliasQualifiedName(CreateGlobalIdentifier(), syntax); + } + + private static IdentifierNameSyntax CreateGlobalIdentifier() => IdentifierName(Token(SyntaxKind.GlobalKeyword)); + + public TypeSyntax VisitBoxedType(BoxedTypeSignature signature, TypeContext context) + { + throw new NotImplementedException(); + } + + public TypeSyntax VisitByReferenceType(ByReferenceTypeSignature signature, TypeContext context) + { + return RefType(signature.BaseType.AcceptVisitor(this, context)); + } + + public TypeSyntax VisitCorLibType(CorLibTypeSignature signature, TypeContext context) + { + // SyntaxKind? kind = signature.ElementType switch + // { + // ElementType.Boolean => SyntaxKind.BoolKeyword, + // ElementType.U1 => SyntaxKind.ByteKeyword, + // ElementType.I1 => SyntaxKind.SByteKeyword, + // ElementType.I4 => SyntaxKind.IntKeyword, + // ElementType.U4 => SyntaxKind.UIntKeyword, + // ElementType.I2 => SyntaxKind.ShortKeyword, + // ElementType.U2 => SyntaxKind.UShortKeyword, + // ElementType.I8 => SyntaxKind.LongKeyword, + // ElementType.U8 => SyntaxKind.ULongKeyword, + // ElementType.R4 => SyntaxKind.FloatKeyword, + // ElementType.R8 => SyntaxKind.DoubleKeyword, + // ElementType.String => SyntaxKind.StringKeyword, + // ElementType.Char => SyntaxKind.CharKeyword, + // ElementType.Object => SyntaxKind.ObjectKeyword, + // ElementType.Void => SyntaxKind.VoidKeyword, + // _ => null, + // }; + // + // if (kind != null) + // { + // return PredefinedType(Token(kind.Value)); + // } + + return VisitTypeDefOrRef(signature.Type, context); + } + + public TypeSyntax VisitCustomModifierType(CustomModifierTypeSignature signature, TypeContext context) + { + return signature.BaseType.AcceptVisitor(this, context); + } + + public TypeSyntax VisitGenericInstanceType(GenericInstanceTypeSignature signature, TypeContext context) + { + return VisitSimpleType(signature.GenericType, context, signature.TypeArguments); + } + + public TypeSyntax VisitGenericParameter(GenericParameterSignature signature, TypeContext context) + { + var nullableAnnotation = context.Transform.TryConsumeNullableTransform(); + if (context.Transform.TryConsumeDynamicTransform() == true) + { + return IdentifierName("dynamic"); + } + + var genericParameter = context.Generic.GetGenericParameter(signature); + + TypeSyntax result = AddInformationTo( + genericParameter != null + ? genericParameter.Name.Value.ToIdentifierName() + : signature.Name.ToIdentifierName(), + signature + ); + + return nullableAnnotation == NullableAnnotation.Annotated ? AddInformationTo(NullableType(result), signature) : result; + } + + public TypeSyntax VisitPinnedType(PinnedTypeSignature signature, TypeContext context) + { + throw new NotImplementedException(); + } + + public TypeSyntax VisitPointerType(PointerTypeSignature signature, TypeContext context) + { + return PointerType(signature.BaseType.AcceptVisitor(this, context)); + } + + public TypeSyntax VisitSentinelType(SentinelTypeSignature signature, TypeContext context) + { + throw new NotImplementedException(); + } + + public TypeSyntax VisitSzArrayType(SzArrayTypeSignature signature, TypeContext context) + { + var nullableAnnotation = context.Transform.TryConsumeNullableTransform(); + if (context.Transform.TryConsumeDynamicTransform() == true) + { + return IdentifierName("dynamic"); + } + + var result = ArrayType(signature.BaseType.AcceptVisitor(this, context), [ArrayRankSpecifier()]); + return nullableAnnotation == NullableAnnotation.Annotated ? AddInformationTo(NullableType(result), signature) : result; + } + + public TypeSyntax VisitArrayType(ArrayTypeSignature signature, TypeContext context) + { + var nullableAnnotation = context.Transform.TryConsumeNullableTransform(); + if (context.Transform.TryConsumeDynamicTransform() == true) + { + return IdentifierName("dynamic"); + } + + var result = ArrayType(signature.BaseType.AcceptVisitor(this, context), SingletonList(ArrayRankSpecifier( + [.. Enumerable.Repeat(OmittedArraySizeExpression(), signature.Rank)] + ))); + return nullableAnnotation == NullableAnnotation.Annotated ? AddInformationTo(NullableType(result), signature) : result; + } + + public TypeSyntax VisitTypeDefOrRef(TypeDefOrRefSignature signature, TypeContext context) + { + return VisitTypeDefOrRef(signature.Type, context); + } + + public TypeSyntax VisitFunctionPointerType(FunctionPointerTypeSignature signature, TypeContext context) + { + FunctionPointerCallingConventionSyntax? callingConventionSyntax = null; + // For varargs there is no C# syntax. You get a use-site diagnostic if you attempt to use it, and just + // making a default-convention symbol is likely good enough. This is only observable through metadata + // that always be uncompilable in C# anyway. + if (signature.Signature.CallingConvention is not CallingConventionAttributes.Default and not CallingConventionAttributes.VarArg) + { + IEnumerable conventionsList = signature.Signature.CallingConvention switch + { + CallingConventionAttributes.C => [GetConventionForString("Cdecl")], + CallingConventionAttributes.StdCall => [GetConventionForString("Stdcall")], + CallingConventionAttributes.ThisCall => [GetConventionForString("Thiscall")], + CallingConventionAttributes.FastCall => [GetConventionForString("Fastcall")], + // CallingConventionAttributes.Unmanaged => + // // All types that come from CallingConventionTypes start with "CallConv". We don't want the prefix for the actual + // // syntax, so strip it off + // signature.Signature.UnmanagedCallingConventionTypes.IsEmpty + // ? null + // : symbol.Signature.UnmanagedCallingConventionTypes.Select(type => GetConventionForString(type.Name["CallConv".Length..])), + + _ => throw new Exception(), + }; + + callingConventionSyntax = FunctionPointerCallingConvention( + Token(SyntaxKind.UnmanagedKeyword), + conventionsList is object + ? FunctionPointerUnmanagedCallingConventionList([.. conventionsList]) + : null); + + static FunctionPointerUnmanagedCallingConventionSyntax GetConventionForString(string identifier) + => FunctionPointerUnmanagedCallingConvention(Identifier(identifier)); + } + + // var parameters = signature.Signature.ParameterTypes.Select(p => (p.Type, RefKindModifiers: CSharpSyntaxGeneratorInternal.GetParameterModifiers(p))) + // .Concat([ + // ( + // Type: signature.Signature.ReturnType, + // RefKindModifiers: CSharpSyntaxGeneratorInternal.GetParameterModifiers(isScoped: false, symbol.Signature.RefKind, isParams: false, forFunctionPointerReturnParameter: true)) + // ]) + // .SelectAsArray(t => FunctionPointerParameter(t.Type.GenerateTypeSyntax()).WithModifiers(t.RefKindModifiers)); + + var parameters = signature.Signature.ParameterTypes + .Append(signature.Signature.ReturnType) + .Select(t => FunctionPointerParameter(VisitTypeDescriptor(t, context))); + + return AddInformationTo(FunctionPointerType(callingConventionSyntax, FunctionPointerParameterList([.. parameters])), signature); + } +} diff --git a/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/DeclarationModifiersExtensions.cs b/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/DeclarationModifiersExtensions.cs new file mode 100644 index 0000000..48c17e9 --- /dev/null +++ b/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/DeclarationModifiersExtensions.cs @@ -0,0 +1,35 @@ +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using CompatUnbreaker.Tool.Utilities.AsmResolver; +using CompatUnbreaker.Utilities.AsmResolver; +using Microsoft.CodeAnalysis.Editing; + +namespace CompatUnbreaker.Tool.SkeletonGeneration.CodeGeneration; + +internal static class DeclarationModifiersExtensions +{ + public static DeclarationModifiers GetModifiers(this IMemberDefinition member) + { + var field = member as FieldDefinition; + var property = member as PropertyDefinition; + var method = member as MethodDefinition; + var type = member as TypeDefinition; + var isConst = field?.IsLiteral == true || field?.Constant != null; + + return DeclarationModifiers.None + .WithIsStatic(member.IsStatic() && !isConst) + .WithIsAbstract(member.IsRoslynAbstract()) + .WithIsReadOnly(field?.IsInitOnly == true || property?.IsReadOnly() == true || type?.IsReadOnly == true || method?.IsReadOnly() == true) + .WithIsVirtual(member.IsRoslynVirtual()) + .WithIsOverride(member.IsOverride()) + .WithIsSealed(member.IsRoslynSealed()) + .WithIsConst(isConst) + // .WithIsUnsafe(symbol.RequiresUnsafeModifier()) TODO + .WithIsRef(field?.Signature?.FieldType is ByReferenceTypeSignature || type?.IsByRefLike == true) + .WithIsVolatile(field?.IsVolatile() == true) + .WithIsExtern(member.IsExtern()) + .WithAsync(method?.IsAsync() == true) + .WithIsRequired(member.IsRequired()) + .WithIsFile(type?.IsFileLocal() == true); + } +} diff --git a/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/NullableAnnotation.cs b/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/NullableAnnotation.cs new file mode 100644 index 0000000..55d2b85 --- /dev/null +++ b/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/NullableAnnotation.cs @@ -0,0 +1,8 @@ +namespace CompatUnbreaker.Tool.SkeletonGeneration.CodeGeneration; + +internal enum NullableAnnotation : byte +{ + Oblivious = 0, + NotAnnotated = 1, + Annotated = 2, +} diff --git a/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/RoslynIdentifierExtensions.cs b/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/RoslynIdentifierExtensions.cs new file mode 100644 index 0000000..3019ee4 --- /dev/null +++ b/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/RoslynIdentifierExtensions.cs @@ -0,0 +1,57 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Simplification; + +namespace CompatUnbreaker.Tool.SkeletonGeneration.CodeGeneration; + +internal static class RoslynIdentifierExtensions +{ + public static string EscapeIdentifier( + this string identifier, + bool isQueryContext = false + ) + { + var nullIndex = identifier.IndexOf('\0', StringComparison.Ordinal); + if (nullIndex >= 0) + { + identifier = identifier[..nullIndex]; + } + + var needsEscaping = SyntaxFacts.GetKeywordKind(identifier) != SyntaxKind.None; + + // Check if we need to escape this contextual keyword + needsEscaping = needsEscaping || (isQueryContext && SyntaxFacts.IsQueryContextualKeyword(SyntaxFacts.GetContextualKeywordKind(identifier))); + + return needsEscaping ? "@" + identifier : identifier; + } + + public static SyntaxToken ToIdentifierToken(this string identifier, bool isQueryContext = false) + { + var escaped = identifier.EscapeIdentifier(isQueryContext); + + if (escaped.Length == 0 || escaped[0] != '@') + { + return SyntaxFactory.Identifier(escaped); + } + + var unescaped = identifier.StartsWith('@') + ? identifier[1..] + : identifier; + + var token = SyntaxFactory.Identifier( + default, SyntaxKind.None, "@" + unescaped, unescaped, default); + + if (!identifier.StartsWith('@')) + { + token = token.WithAdditionalAnnotations(Simplifier.Annotation); + } + + return token; + } + + public static IdentifierNameSyntax ToIdentifierName(this string identifier) + { + return SyntaxFactory.IdentifierName(identifier.ToIdentifierToken()); + } +} diff --git a/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Attributes.cs b/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Attributes.cs new file mode 100644 index 0000000..5ddfcd6 --- /dev/null +++ b/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Attributes.cs @@ -0,0 +1,184 @@ +using System.Diagnostics; +using AsmResolver; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using AsmResolver.PE.DotNet.Metadata.Tables; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Editing; + +namespace CompatUnbreaker.Tool.SkeletonGeneration.CodeGeneration; + +internal static partial class SyntaxGeneratorExtensions +{ + public static SyntaxNode AddAttributes(this SyntaxGenerator syntaxGenerator, SyntaxNode declaration, IEnumerable attributes) + { + return syntaxGenerator.AddAttributes(declaration, syntaxGenerator.Attributes(attributes)); + } + + public static IEnumerable Attributes(this SyntaxGenerator syntaxGenerator, IEnumerable attributes) + { + return attributes.Where(a => !IsReserved(a)).Select(syntaxGenerator.Attribute); + } + + private static bool IsReserved(CustomAttribute attribute) + { + var type = attribute.Constructor?.DeclaringType; + if (type == null) return false; + + if (type.IsTypeOf("System", "ObsoleteAttribute")) + { + const string ByRefLikeMarker = "Types with embedded references are not supported in this version of your compiler."; + const string RequiredMembersMarker = "Constructors of types with required members are not supported in this version of your compiler."; + + return attribute.Signature?.FixedArguments.FirstOrDefault()?.Element is Utf8String { Value: ByRefLikeMarker or RequiredMembersMarker }; + } + + if (type.Namespace == "System.Runtime.CompilerServices") + { + switch (type.Name) + { + case "DynamicAttribute": + case "IsReadOnlyAttribute": + case "RequiresLocationAttribute": + case "IsUnmanagedAttribute": + case "IsByRefLikeAttribute": + case "CompilerFeatureRequiredAttribute": + case "TupleElementNamesAttribute": + case "NullableAttribute": + case "NullableContextAttribute": + case "NullablePublicOnlyAttribute": + // case "NativeIntegerAttribute": TODO roslyn doesn't emit this for me? huh? + case "ExtensionAttribute": + case "RequiredMemberAttribute": + case "ScopedRefAttribute": + case "RefSafetyRulesAttribute": + case "ParamCollectionAttribute": + return true; + } + } + else if (type.Namespace == "System") + { + switch (type.Name) + { + case "ParamArrayAttribute": + return true; + } + } + + return false; + } + + public static SyntaxNode Attribute(this SyntaxGenerator syntaxGenerator, CustomAttribute attribute) + { + ArgumentNullException.ThrowIfNull(attribute.Constructor); + ArgumentNullException.ThrowIfNull(attribute.Constructor.DeclaringType); + ArgumentNullException.ThrowIfNull(attribute.Constructor.Signature); + ArgumentNullException.ThrowIfNull(attribute.Signature); + + var args = attribute.Signature.FixedArguments.Select((a, i) => + { + var parameterType = attribute.Constructor.Signature.ParameterTypes[i]; + Debug.Assert(SignatureComparer.Default.Equals(parameterType, a.ArgumentType)); + return syntaxGenerator.AttributeArgument(syntaxGenerator.AttributeArgumentExpression(a)); + }) + .Concat(attribute.Signature.NamedArguments.Select(n => syntaxGenerator.AttributeArgument(n.MemberName, syntaxGenerator.AttributeArgumentExpression(n.Argument)))) + .ToArray(); + + return syntaxGenerator.Attribute( + name: syntaxGenerator.TypeExpression(attribute.Constructor.DeclaringType, TypeContext.Empty), + attributeArguments: args.Length > 0 ? args : null + ); + } + + private static SyntaxNode AttributeArgumentExpression(this SyntaxGenerator syntaxGenerator, CustomAttributeArgument argument) + { + var value = argument.ArgumentType.ElementType == ElementType.SzArray + ? (argument.IsNullArray ? null : argument.Elements) + : argument.Element; + + var typeContext = TypeContext.Empty; // CAs can't use generics + return syntaxGenerator.LiteralExpression(argument.ArgumentType, value, typeContext); + } + + public static SyntaxNode LiteralExpression(this SyntaxGenerator syntaxGenerator, TypeSignature typeSignature, object? value, TypeContext typeContext) + { + if (value == null) + { + if (!typeSignature.IsValueType) + { + return syntaxGenerator.CastExpression( + syntaxGenerator.NullableTypeExpression(syntaxGenerator.TypeExpression(typeSignature, typeContext)), + syntaxGenerator.NullLiteralExpression() + ); + } + + return syntaxGenerator.DefaultExpression(syntaxGenerator.TypeExpression(typeSignature, typeContext)); + } + + if (value is BoxedArgument boxedArgument) + { + // null objects get serialized as boxed null strings + if (typeSignature.ElementType == ElementType.Object && boxedArgument.Type.ElementType == ElementType.String && boxedArgument.Value == null) + { + return syntaxGenerator.LiteralExpression(typeSignature, null, typeContext); + } + + return syntaxGenerator.CastExpression( + syntaxGenerator.TypeExpression(typeSignature, typeContext), + syntaxGenerator.LiteralExpression(boxedArgument.Type, boxedArgument.Value, typeContext) + ); + } + + if (typeSignature.ElementType == ElementType.String) + { + return syntaxGenerator.LiteralExpression(((Utf8String) value).Value); + } + + if (typeSignature.ElementType.IsPrimitive()) + { + return syntaxGenerator.LiteralExpression(value); + } + + if (typeSignature is SzArrayTypeSignature arrayTypeSignature) + { + var baseType = arrayTypeSignature.BaseType; + var values = (IList) value; + + return syntaxGenerator.ArrayCreationExpression( + syntaxGenerator.TypeExpression(baseType, typeContext), + values.Select(o => syntaxGenerator.LiteralExpression(baseType, o, typeContext)) + ); + } + + if (typeSignature.IsTypeOf("System", "Type")) + { + var type = (ITypeDescriptor) value; + return syntaxGenerator.TypeOfExpression(syntaxGenerator.TypeExpression(type, typeContext)); + } + + if (typeSignature.Resolve() is { IsEnum: true } enumType) + { + return syntaxGenerator.CreateEnumConstantValue(enumType, value, typeContext); + } + + throw new NotSupportedException($"Couldn't generate a LiteralExpression for type '{typeSignature}' with value '{value}'"); + } + + private static bool IsPrimitive(this ElementType elementType) + { + switch (elementType) + { + case ElementType.Void: + case ElementType.Boolean: + case ElementType.Char: + case ElementType.I1 or ElementType.I2 or ElementType.I4 or ElementType.I8: + case ElementType.U1 or ElementType.U2 or ElementType.U4 or ElementType.U8: + case ElementType.R4 or ElementType.R8: + case ElementType.I or ElementType.U: + return true; + + default: + return false; + } + } +} diff --git a/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.EnumValues.cs b/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.EnumValues.cs new file mode 100644 index 0000000..0d9c855 --- /dev/null +++ b/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.EnumValues.cs @@ -0,0 +1,216 @@ +using AsmResolver.DotNet; +using AsmResolver.PE.DotNet.Metadata.Tables; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Editing; + +namespace CompatUnbreaker.Tool.SkeletonGeneration.CodeGeneration; + +internal static partial class SyntaxGeneratorExtensions +{ + public static SyntaxNode CreateEnumConstantValue(this SyntaxGenerator syntaxGenerator, TypeDefinition enumType, object constantValue, TypeContext typeContext) + { + if (enumType.HasCustomAttribute("System", "FlagsAttribute")) + { + return syntaxGenerator.CreateFlagsEnumConstantValue(enumType, constantValue, typeContext); + } + + return syntaxGenerator.CreateNonFlagsEnumConstantValue(enumType, constantValue, typeContext); + } + + private static SyntaxNode CreateNonFlagsEnumConstantValue(this SyntaxGenerator syntaxGenerator, TypeDefinition enumType, object constantValue, TypeContext typeContext) + { + var underlyingType = enumType.GetEnumUnderlyingType(); + var constantValueULong = ConvertUnderlyingValueToUInt64(underlyingType.ElementType, constantValue); + + foreach (var field in enumType.Fields) + { + if (field is { Constant: not null }) + { + var fieldValue = ConvertUnderlyingValueToUInt64(underlyingType.ElementType, field.Constant.InterpretData()); + if (constantValueULong == fieldValue) + return syntaxGenerator.CreateMemberAccessExpression(field, enumType, underlyingType.ElementType, typeContext); + } + } + + // Otherwise, just add the enum as a literal. + return syntaxGenerator.CreateExplicitlyCastedLiteralValue(enumType, underlyingType.ElementType, constantValue, typeContext); + } + + private static SyntaxNode CreateMemberAccessExpression( + this SyntaxGenerator syntaxGenerator, + FieldDefinition field, + TypeDefinition enumType, + ElementType underlyingSpecialType, + TypeContext typeContext + ) + { + if (SyntaxFacts.IsValidIdentifier(field.Name)) + { + return syntaxGenerator.MemberAccessExpression(syntaxGenerator.TypeExpression(enumType, typeContext), syntaxGenerator.IdentifierName(field.Name)); + } + + return syntaxGenerator.CreateExplicitlyCastedLiteralValue(enumType, underlyingSpecialType, field.Constant.InterpretData(), typeContext); + } + + private static SyntaxNode CreateExplicitlyCastedLiteralValue( + this SyntaxGenerator syntaxGenerator, + TypeDefinition enumType, + ElementType underlyingSpecialType, + object constantValue, + TypeContext typeContext + ) + { + var expression = syntaxGenerator.LiteralExpression(constantValue); + + var constantValueULong = ConvertUnderlyingValueToUInt64(underlyingSpecialType, constantValue); + if (constantValueULong == 0) + { + // 0 is always convertible to an enum type without needing a cast. + return expression; + } + + return syntaxGenerator.CastExpression(syntaxGenerator.TypeExpression(enumType, typeContext), expression); + } + + private static ulong ConvertUnderlyingValueToUInt64(ElementType enumUnderlyingType, object value) + { + unchecked + { + return enumUnderlyingType switch + { + ElementType.I1 => (ulong) (sbyte) value, + ElementType.I2 => (ulong) (short) value, + ElementType.I4 => (ulong) (int) value, + ElementType.I8 => (ulong) (long) value, + ElementType.U1 => (byte) value, + ElementType.U2 => (ushort) value, + ElementType.U4 => (uint) value, + ElementType.U8 => (ulong) value, + _ => throw new ArgumentOutOfRangeException(nameof(enumUnderlyingType), enumUnderlyingType, null), + }; + } + } + + private static SyntaxNode CreateFlagsEnumConstantValue(this SyntaxGenerator syntaxGenerator, TypeDefinition enumType, object constantValue, TypeContext typeContext) + { + // These values are sorted by value. Don't change this. + var allFieldsAndValues = new List<(FieldDefinition field, ulong value)>(); + GetSortedEnumFieldsAndValues(enumType, allFieldsAndValues); + + var usedFieldsAndValues = new List<(FieldDefinition field, ulong value)>(); + return syntaxGenerator.CreateFlagsEnumConstantValue(enumType, constantValue, allFieldsAndValues, usedFieldsAndValues, typeContext); + } + + private static SyntaxNode CreateFlagsEnumConstantValue( + this SyntaxGenerator syntaxGenerator, + TypeDefinition enumType, + object constantValue, + List<(FieldDefinition field, ulong value)> allFieldsAndValues, + List<(FieldDefinition field, ulong value)> usedFieldsAndValues, + TypeContext typeContext + ) + { + var underlyingSpecialType = enumType.GetEnumUnderlyingType(); + var constantValueULong = ConvertUnderlyingValueToUInt64(underlyingSpecialType.ElementType, constantValue); + + var result = constantValueULong; + + // We will not optimize this code further to keep it maintainable. There are some + // boundary checks that can be applied to minimize the comparisons required. This code + // works the same for the best/worst case. In general the number of items in an enum are + // sufficiently small and not worth the optimization. + for (var index = allFieldsAndValues.Count - 1; index >= 0 && result != 0; index--) + { + var fieldAndValue = allFieldsAndValues[index]; + var valueAtIndex = fieldAndValue.value; + + if (valueAtIndex != 0 && (result & valueAtIndex) == valueAtIndex) + { + result -= valueAtIndex; + usedFieldsAndValues.Add(fieldAndValue); + } + } + + // We were able to represent this number as a bitwise OR of valid flags. + if (result == 0 && usedFieldsAndValues.Count > 0) + { + // We want to emit the fields in lower to higher value. So we walk backward. + SyntaxNode? finalNode = null; + for (var i = usedFieldsAndValues.Count - 1; i >= 0; i--) + { + var field = usedFieldsAndValues[i]; + var node = syntaxGenerator.CreateMemberAccessExpression(field.field, enumType, underlyingSpecialType.ElementType, typeContext); + if (finalNode == null) + { + finalNode = node; + } + else + { + finalNode = syntaxGenerator.BitwiseOrExpression(finalNode, node); + } + } + + return finalNode; + } + + // We couldn't find fields to OR together to make the value. + + // If we had 0 as the value, and there's an enum value equal to 0, then use that. + var zeroField = GetZeroField(allFieldsAndValues); + if (constantValueULong == 0 && zeroField != null) + { + return syntaxGenerator.CreateMemberAccessExpression(zeroField, enumType, underlyingSpecialType.ElementType, typeContext); + } + else + { + // Add anything else in as a literal value. + return syntaxGenerator.CreateExplicitlyCastedLiteralValue(enumType, underlyingSpecialType.ElementType, constantValue, typeContext); + } + } + + private static FieldDefinition? GetZeroField(List<(FieldDefinition field, ulong value)> allFieldsAndValues) + { + for (var i = allFieldsAndValues.Count - 1; i >= 0; i--) + { + var (field, value) = allFieldsAndValues[i]; + if (value == 0) + { + return field; + } + } + + return null; + } + + private static void GetSortedEnumFieldsAndValues( + TypeDefinition enumType, + List<(FieldDefinition field, ulong value)> allFieldsAndValues + ) + { + var underlyingType = enumType.GetEnumUnderlyingType(); + foreach (var field in enumType.Fields) + { + if (field is { Constant: not null }) + { + var value = ConvertUnderlyingValueToUInt64(underlyingType.ElementType, field.Constant.InterpretData()); + allFieldsAndValues.Add((field, value)); + } + } + + allFieldsAndValues.Sort(Compare); + + static int Compare((FieldDefinition field, ulong value) x, (FieldDefinition field, ulong value) y) + { + unchecked + { + return + (long) x.value < (long) y.value + ? -1 + : (long) x.value > (long) y.value + ? 1 + : -x.field.Name.CompareTo(y.field.Name); + } + } + } +} diff --git a/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Events.cs b/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Events.cs new file mode 100644 index 0000000..b168420 --- /dev/null +++ b/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Events.cs @@ -0,0 +1,49 @@ +using AsmResolver.DotNet; +using CompatUnbreaker.Tool.Utilities.AsmResolver; +using CompatUnbreaker.Utilities.AsmResolver; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Editing; +using Accessibility = Microsoft.CodeAnalysis.Accessibility; + +namespace CompatUnbreaker.Tool.SkeletonGeneration.CodeGeneration; + +internal static partial class SyntaxGeneratorExtensions +{ + public static SyntaxNode EventDeclaration(this SyntaxGenerator syntaxGenerator, EventDefinition @event) + { + ArgumentNullException.ThrowIfNull(@event.Name); + ArgumentNullException.ThrowIfNull(@event.EventType); + + var typeContext = TypeContext.From(@event, @event); + + var isAuto = @event.AddMethod?.IsCompilerGenerated() == true; + + SyntaxNode declaration; + + if (isAuto) + { + declaration = syntaxGenerator.EventDeclaration( + @event.Name, + syntaxGenerator.TypeExpression(@event.EventType, typeContext), + (Accessibility) @event.GetAccessibility(), + @event.GetModifiers() + ); + } + else + { + declaration = syntaxGenerator.CustomEventDeclaration( + @event.Name, + syntaxGenerator.TypeExpression(@event.EventType, typeContext), + (Accessibility) @event.GetAccessibility(), + @event.GetModifiers() + ); + } + + // TODO + // if (symbol.ExplicitInterfaceImplementations.Length > 0) + // { + // ev = this.WithExplicitInterfaceImplementations(ev, ImmutableArray.CastUp(symbol.ExplicitInterfaceImplementations)); + // } + return syntaxGenerator.AddAttributes(declaration, @event.CustomAttributes); + } +} diff --git a/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Fields.cs b/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Fields.cs new file mode 100644 index 0000000..fbd1b42 --- /dev/null +++ b/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Fields.cs @@ -0,0 +1,39 @@ +using AsmResolver.DotNet; +using CompatUnbreaker.Utilities.AsmResolver; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Editing; +using Accessibility = Microsoft.CodeAnalysis.Accessibility; + +namespace CompatUnbreaker.Tool.SkeletonGeneration.CodeGeneration; + +internal static partial class SyntaxGeneratorExtensions +{ + public static SyntaxNode FieldDeclaration(this SyntaxGenerator syntaxGenerator, FieldDefinition field) + { + var typeContext = TypeContext.From(field, field); + var initializer = field.Constant is { } constant + ? syntaxGenerator.LiteralExpression(field.DeclaringType?.IsEnum == true ? field.DeclaringType.GetEnumUnderlyingType() : field.Signature.FieldType, constant.InterpretData(), typeContext) + : null; + + return syntaxGenerator.FieldDeclaration(field, initializer); + } + + public static SyntaxNode FieldDeclaration(this SyntaxGenerator syntaxGenerator, FieldDefinition field, SyntaxNode? initializer) + { + ArgumentNullException.ThrowIfNull(field.Name); + ArgumentNullException.ThrowIfNull(field.Signature); + + var typeContext = TypeContext.From(field, field); + + return syntaxGenerator.AddAttributes( + syntaxGenerator.FieldDeclaration( + field.Name, + syntaxGenerator.TypeExpression(field.Signature.FieldType, typeContext), + (Accessibility) field.GetAccessibility(), + field.GetModifiers(), + initializer + ), + field.CustomAttributes + ); + } +} diff --git a/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Generics.cs b/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Generics.cs new file mode 100644 index 0000000..9a9aea1 --- /dev/null +++ b/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Generics.cs @@ -0,0 +1,132 @@ +using AsmResolver.DotNet; +using AsmResolver.PE.DotNet.Metadata.Tables; +using CompatUnbreaker.Tool.Utilities.AsmResolver; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace CompatUnbreaker.Tool.SkeletonGeneration.CodeGeneration; + +internal static partial class SyntaxGeneratorExtensions +{ + public static SyntaxNode WithTypeParametersAndConstraints(this SyntaxGenerator syntaxGenerator, SyntaxNode declaration, IList typeParameters, TypeContext typeContext) + { + if (typeParameters.Count <= 0) + return declaration; + + declaration = WithTypeParameters(syntaxGenerator, declaration, typeParameters.Select(syntaxGenerator.TypeParameter)); + + foreach (var typeParameter in typeParameters) + { + ArgumentNullException.ThrowIfNull(typeParameter.Name); + + var parameterContext = typeContext.WithTransformsAttributeProvider(typeParameter); + var hasNotNullConstraint = parameterContext.Transform.TryConsumeNullableTransform() == NullableAnnotation.NotAnnotated; + + if (HasSomeConstraint(typeParameter) || hasNotNullConstraint) + { + var constraints = typeParameter.Constraints + .Where(c => + c.Constraint != null && + (!typeParameter.HasNotNullableValueTypeConstraint || !c.Constraint.ToTypeSignature().StripModifiers().IsTypeOf("System", "ValueType")) + ); + + var kinds = SpecialTypeConstraintKind.None; + if (typeParameter.HasDefaultConstructorConstraint && !typeParameter.HasNotNullableValueTypeConstraint) + kinds |= SpecialTypeConstraintKind.Constructor; + if (typeParameter.HasReferenceTypeConstraint && hasNotNullConstraint) + kinds |= SpecialTypeConstraintKind.ReferenceType; + if (typeParameter.HasNotNullableValueTypeConstraint) + kinds |= SpecialTypeConstraintKind.ValueType; + + declaration = syntaxGenerator.WithTypeConstraint( + declaration, + typeParameter.Name, + kinds, + isUnamangedType: typeParameter.HasUnmanagedTypeConstraint(), + types: constraints.Select(c => syntaxGenerator.TypeExpression(c.Constraint!, typeContext)) + ); + + if (hasNotNullConstraint) + { + if (!typeParameter.HasReferenceTypeConstraint) + { + declaration = AddTypeConstraints( + declaration, + typeParameter.Name, + [TypeConstraint(IdentifierName("notnull"))], + true + ); + } + } + else if (typeParameter.HasReferenceTypeConstraint) + { + declaration = AddTypeConstraints( + declaration, + typeParameter.Name, + [ClassOrStructConstraint(SyntaxKind.ClassConstraint).WithQuestionToken(Token(SyntaxKind.QuestionToken))], + true + ); + } + + if (typeParameter.HasAllowByRefLike) + { + declaration = AddTypeConstraints( + declaration, + typeParameter.Name, + [AllowsConstraintClause([RefStructConstraint()])] + ); + } + } + } + + return declaration; + } + + private static bool HasSomeConstraint(GenericParameter typeParameter) + { + return typeParameter.HasDefaultConstructorConstraint || + typeParameter.HasReferenceTypeConstraint || + typeParameter.HasNotNullableValueTypeConstraint || + typeParameter.Constraints.Count > 0; + } + + private static SyntaxNode TypeParameter(this SyntaxGenerator syntaxGenerator, GenericParameter typeParameter) + { + return SyntaxFactory.TypeParameter( + attributeLists: [.. syntaxGenerator.Attributes(typeParameter.CustomAttributes).Cast()], + varianceKeyword: typeParameter.Variance switch + { + GenericParameterAttributes.Contravariant => Token(SyntaxKind.InKeyword), + GenericParameterAttributes.Covariant => Token(SyntaxKind.OutKeyword), + _ => default, + }, + Identifier(typeParameter.Name!) + ); + } + + private static SyntaxNode AddTypeConstraints(SyntaxNode declaration, string typeParameterName, SeparatedSyntaxList constraints, bool insert = false) + { + return declaration switch + { + MethodDeclarationSyntax method => method.WithConstraintClauses(AddTypeConstraints(method.ConstraintClauses, typeParameterName, constraints, insert)), + TypeDeclarationSyntax type => type.WithConstraintClauses(AddTypeConstraints(type.ConstraintClauses, typeParameterName, constraints, insert)), + DelegateDeclarationSyntax @delegate => @delegate.WithConstraintClauses(AddTypeConstraints(@delegate.ConstraintClauses, typeParameterName, constraints, insert)), + _ => declaration, + }; + } + + private static SyntaxList AddTypeConstraints(SyntaxList clauses, string typeParameterName, SeparatedSyntaxList constraints, bool insert) + { + var clause = clauses.FirstOrDefault(c => c.Name.Identifier.ToString() == typeParameterName); + + if (clause == null) + { + return clauses.Add(TypeParameterConstraintClause(typeParameterName.ToIdentifierName(), constraints)); + } + + return clauses.Replace(clause, clause.WithConstraints(insert ? clause.Constraints.InsertRange(0, constraints) : clause.Constraints.AddRange(constraints))); + } +} diff --git a/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Methods.cs b/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Methods.cs new file mode 100644 index 0000000..0ce64e7 --- /dev/null +++ b/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Methods.cs @@ -0,0 +1,428 @@ +using System.Runtime.InteropServices; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Collections; +using AsmResolver.DotNet.Signatures; +using AsmResolver.PE.DotNet.Metadata.Tables; +using CompatUnbreaker.Tool.Utilities.AsmResolver; +using CompatUnbreaker.Utilities.AsmResolver; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using Accessibility = Microsoft.CodeAnalysis.Accessibility; + +namespace CompatUnbreaker.Tool.SkeletonGeneration.CodeGeneration; + +internal static partial class SyntaxGeneratorExtensions +{ + // private static bool IsValidStaticUserDefinedOperatorSignature(this MethodDefinition method, int parameterCount) + // { + // if (method.Signature.ReturnType.ElementType == ElementType.Void || + // method.GenericParameters.Count > 0 || + // method.Signature.CallingConvention == CallingConventionAttributes.VarArg || + // method.Parameters.Count != parameterCount || + // method.IsParams()) + // { + // return false; + // } + // + // return method.HasValidOperatorParameterRefKinds(); + // } + // + // private static bool HasValidOperatorParameterRefKinds(this MethodDefinition method) + // { + // if (this.ParameterRefKinds.IsDefault) + // { + // return true; + // } + // + // foreach (var kind in this.ParameterRefKinds) + // { + // switch (kind) + // { + // case RefKind.None: + // case RefKind.In: + // continue; + // case RefKind.Out: + // case RefKind.Ref: + // case RefKind.RefReadOnlyParameter: + // return false; + // default: + // throw ExceptionUtilities.UnexpectedValue(kind); + // } + // } + // + // return true; + // } + + public static SyntaxNode MethodDeclaration(this SyntaxGenerator syntaxGenerator, MethodDefinition method, IEnumerable? statements = null) + { + if (method.IsConstructor) + { + return syntaxGenerator.ConstructorDeclaration(method); + } + + if (method.IsDestructor()) + { + throw new NotImplementedException(); + } + + if (method.IsSpecialName && SyntaxFacts.GetOperatorKind(method.Name) != SyntaxKind.None) + { + var returnTypeContext2 = TypeContext.From(method, method.Parameters.ReturnParameter.GetOrCreateDefinition()); + var decl2 = syntaxGenerator.OperatorDeclaration( + method.Name, + isImplicitConversion: method.Name.Value is WellKnownMemberNames.ImplicitConversionName, + parameters: method.Parameters.Select(p => syntaxGenerator.ParameterDeclaration(p)), + returnType: method.Signature.ReturnType.ElementType == ElementType.Void ? null : TypeExpression(syntaxGenerator, method.Signature.ReturnType, method.Parameters.ReturnParameter.GetRefKind(), returnTypeContext2), + accessibility: (Accessibility) method.GetAccessibility(), + modifiers: method.GetModifiers(), + statements: statements + ); + + return decl2; + } + + // if (method.MethodKind == MethodKind.Destructor) + // { + // return syntaxGenerator.DestructorDeclaration(method); + // } + // + // if (method.MethodKind is MethodKind.UserDefinedOperator or MethodKind.Conversion) + // { + // return syntaxGenerator.OperatorDeclaration(method); + // } + + var typeContext = TypeContext.From(method, method); + + var returnTypeContext = TypeContext.From(method, method.Parameters.ReturnParameter.GetOrCreateDefinition()); + + var decl = MethodDeclaration( + syntaxGenerator, + method.Name, + typeParameters: method.GenericParameters.Select(syntaxGenerator.TypeParameter), + parameters: method.Parameters.Select(p => syntaxGenerator.ParameterDeclaration(p)), + returnType: method.Signature.ReturnType.ElementType == ElementType.Void ? null : TypeExpression(syntaxGenerator, method.Signature.ReturnType, method.Parameters.ReturnParameter.GetRefKind(), returnTypeContext), + accessibility: (Accessibility) method.GetAccessibility(), + modifiers: method.GetModifiers(), + statements: statements + ); + + if (method.GenericParameters.Count > 0) + { + // // Overrides are special. Specifically, in an override, if a type parameter has no constraints, then we + // // want to still add `where T : default` if that type parameter is used with NRT (e.g. `T?`) that way + // // the language can distinguish if this is a Nullable Value Type or not. + // if (method.IsOverride()) + // { + // foreach (var typeParameter in method.GenericParameters) + // { + // if (HasNullableAnnotation(typeParameter, method)) + // { + // if (!HasSomeConstraint(typeParameter)) + // { + // // if there are no constraints, add `where T : default` so it's known this not an NVT + // // and is just an unconstrained type parameter. + // decl = WithDefaultConstraint(decl, typeParameter.Name); + // } + // else if (!typeParameter.HasValueTypeConstraint) + // { + // // if there are some constraints, add `where T : class` so it's known this is not an NVT + // // and must specifically be some reference type. + // decl = WithTypeConstraint(decl, typeParameter.Name, SpecialTypeConstraintKind.ReferenceType); + // } + // } + // } + // } + // else + { + decl = syntaxGenerator.WithTypeParametersAndConstraints(decl, method.GenericParameters, typeContext); + } + } + + // if (method.ExplicitInterfaceImplementations.Length > 0) + // { + // decl = this.WithExplicitInterfaceImplementations(decl, + // ImmutableArray.CastUp(method.ExplicitInterfaceImplementations)); + // } + + if (method.IsPInvokeImpl) + { + var implementationMap = method.ImplementationMap; + + var dllName = implementationMap.Scope.Name.Value; + + var arguments = new List + { + syntaxGenerator.AttributeArgument(syntaxGenerator.LiteralExpression(dllName)), + }; + + if (implementationMap.Name != method.Name) + { + arguments.Add(syntaxGenerator.AttributeArgument( + "EntryPoint", + syntaxGenerator.LiteralExpression(implementationMap.Name.Value) + )); + } + + var charSet = implementationMap.Attributes & ImplementationMapAttributes.CharSetMask; + if (charSet != ImplementationMapAttributes.CharSetNotSpec) + { + arguments.Add(syntaxGenerator.AttributeArgument( + "CharSet", + syntaxGenerator.MemberAccessExpression(SyntaxFactory.ParseTypeName("System.Runtime.InteropServices.CharSet"), charSet switch + { + ImplementationMapAttributes.CharSetAnsi => nameof(CharSet.Ansi), + ImplementationMapAttributes.CharSetUnicode => nameof(CharSet.Unicode), + ImplementationMapAttributes.CharSetAuto => nameof(CharSet.Auto), + _ => throw new ArgumentOutOfRangeException(), + }) + )); + } + + if ((implementationMap.Attributes & ImplementationMapAttributes.SupportsLastError) != 0) + { + arguments.Add(syntaxGenerator.AttributeArgument("SetLastError", syntaxGenerator.TrueLiteralExpression())); + } + + if ((implementationMap.Attributes & ImplementationMapAttributes.NoMangle) != 0) + { + arguments.Add(syntaxGenerator.AttributeArgument("ExactSpelling", syntaxGenerator.TrueLiteralExpression())); + } + + var callingConvention = implementationMap.Attributes & ImplementationMapAttributes.CallConvMask; + if (callingConvention != ImplementationMapAttributes.CallConvWinapi) + { + arguments.Add(syntaxGenerator.AttributeArgument( + "CallingConvention", + syntaxGenerator.MemberAccessExpression(SyntaxFactory.ParseTypeName("System.Runtime.InteropServices.CallingConvention"), callingConvention switch + { + ImplementationMapAttributes.CallConvCdecl => nameof(CallingConvention.Cdecl), + ImplementationMapAttributes.CallConvStdcall => nameof(CallingConvention.StdCall), + ImplementationMapAttributes.CallConvThiscall => nameof(CallingConvention.ThisCall), + ImplementationMapAttributes.CallConvFastcall => nameof(CallingConvention.FastCall), + _ => throw new ArgumentOutOfRangeException(), + }) + )); + } + + + var bestFitMapping = implementationMap.Attributes & ImplementationMapAttributes.BestFitMask; + if (bestFitMapping != ImplementationMapAttributes.BestFitUseAssem) + { + arguments.Add(syntaxGenerator.AttributeArgument( + "BestFitMapping", + bestFitMapping switch + { + ImplementationMapAttributes.BestFitEnabled => syntaxGenerator.TrueLiteralExpression(), + ImplementationMapAttributes.BestFitDisabled => syntaxGenerator.FalseLiteralExpression(), + _ => throw new ArgumentOutOfRangeException(), + }) + ); + } + + if (!method.PreserveSignature) + { + arguments.Add(syntaxGenerator.AttributeArgument("PreserveSig", syntaxGenerator.FalseLiteralExpression())); + } + + var throwOnUnmappableChar = implementationMap.Attributes & ImplementationMapAttributes.ThrowOnUnmappableCharMask; + if (throwOnUnmappableChar != ImplementationMapAttributes.ThrowOnUnmappableCharUseAssem) + { + arguments.Add(syntaxGenerator.AttributeArgument( + "ThrowOnUnmappableChar", + throwOnUnmappableChar switch + { + ImplementationMapAttributes.ThrowOnUnmappableCharEnabled => syntaxGenerator.TrueLiteralExpression(), + ImplementationMapAttributes.ThrowOnUnmappableCharDisabled => syntaxGenerator.FalseLiteralExpression(), + _ => throw new ArgumentOutOfRangeException(), + }) + ); + } + + decl = syntaxGenerator.AddAttributes(decl, syntaxGenerator.Attribute( + "System.Runtime.InteropServices.DllImportAttribute", + arguments + )); + } + + if (method.IsExtern()) + { + decl = ((BaseMethodDeclarationSyntax) decl) + .WithBody(null) + .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken).WithTrailingTrivia(SyntaxFactory.LineFeed)) + .WithLeadingTrivia(SyntaxFactory.LineFeed); + } + + var attributes = syntaxGenerator.Attributes(method.CustomAttributes) + .Concat( + syntaxGenerator.Attributes(method.Parameters.ReturnParameter.GetOrCreateDefinition().CustomAttributes) + .Cast() + .Select(a => a.WithTarget(SyntaxFactory.AttributeTargetSpecifier(SyntaxFactory.Token(SyntaxKind.ReturnKeyword)))) + ); + + return syntaxGenerator.AddAttributes(decl, attributes); + + // bool HasNullableAnnotation(ITypeParameterSymbol typeParameter, IMethodSymbol method) + // { + // return method.ReturnType.GetReferencedTypeParameters().Any(t => IsNullableAnnotatedTypeParameter(typeParameter, t)) || + // method.Parameters.Any(p => p.Type.GetReferencedTypeParameters().Any(t => IsNullableAnnotatedTypeParameter(typeParameter, t))); + // } + // + // static bool IsNullableAnnotatedTypeParameter(ITypeParameterSymbol typeParameter, ITypeParameterSymbol current) + // { + // return Equals(current, typeParameter) && current.NullableAnnotation == NullableAnnotation.Annotated; + // } + } + + private static SyntaxNode ConstructorDeclaration( + this SyntaxGenerator syntaxGenerator, + MethodDefinition constructorMethod, + IEnumerable? baseConstructorArguments = null, + IEnumerable? statements = null + ) + { + return syntaxGenerator.AddAttributes( + syntaxGenerator.ConstructorDeclaration( + constructorMethod.DeclaringType != null ? constructorMethod.DeclaringType.Name : "New", + constructorMethod.Parameters.Select(p => syntaxGenerator.ParameterDeclaration(p)), + (Accessibility) constructorMethod.GetAccessibility(), + constructorMethod.GetModifiers(), + baseConstructorArguments, + statements + ), + constructorMethod.CustomAttributes + ); + } + + public static SyntaxNode ParameterDeclaration(this SyntaxGenerator syntaxGenerator, Parameter symbol, SyntaxNode? initializer = null) + { + var definition = symbol.GetOrCreateDefinition(); + + var refKind = symbol.GetRefKind(); + + var typeContext = TypeContext.From(definition.Method, definition); + var type = syntaxGenerator.TypeExpression(symbol.ParameterType, typeContext); + if (type is RefTypeSyntax refType) + { + type = refType.Type; + } + + var isParams = definition.IsParams(); + + return syntaxGenerator.AddAttributes( + syntaxGenerator.ParameterDeclaration( + symbol.Name, + type, + initializer ?? (definition.Constant is { } constant + ? syntaxGenerator.LiteralExpression(symbol.ParameterType, constant.InterpretData(), typeContext) + : null), + refKind, + isExtension: symbol.Index == 0 && definition.Method?.IsExtension() == true, + isParams, + isScoped: symbol.IsScoped(refKind) && !isParams + ), + definition.CustomAttributes + ); + } + + private static RefKind GetRefKind(this Parameter parameter) + { + if (parameter.ParameterType.StripModifiers() is ByReferenceTypeSignature) + { + var definition = parameter.GetOrCreateDefinition(); + ArgumentNullException.ThrowIfNull(definition.Method); + + var isReturn = parameter == definition.Method.Parameters.ReturnParameter; + + if (definition.IsOut) + { + return RefKind.Out; + } + + if (!isReturn && definition.HasCustomAttribute("System.Runtime.CompilerServices", "RequiresLocationAttribute")) + { + return RefKind.RefReadOnlyParameter; + } + + if (definition.HasIsReadOnlyAttribute()) + { + return RefKind.In; + } + + return RefKind.Ref; + } + + return RefKind.None; + } + + private static bool IsScoped(this Parameter parameter, RefKind refKind) + { + var scopedKind = parameter.GetScopedKind(refKind); + + if (refKind is RefKind.Ref or RefKind.In or RefKind.RefReadOnlyParameter && scopedKind == ScopedKind.ScopedRef) + return true; + + bool isByRefLike; + + if (parameter.ParameterType is ByReferenceTypeSignature) + { + isByRefLike = true; + } + else if (parameter.ParameterType is GenericParameterSignature genericParameterSignature) + { + var method = parameter.GetOrCreateDefinition().Method; + + var genericParameters = genericParameterSignature.ParameterType switch + { + GenericParameterType.Type => method.DeclaringType.GenericParameters, + GenericParameterType.Method => method.GenericParameters, + _ => throw new ArgumentOutOfRangeException(), + }; + + var genericParameter = genericParameters[genericParameterSignature.Index]; + + isByRefLike = genericParameter.HasAllowByRefLike; + } + else if (parameter.ParameterType is TypeDefOrRefSignature or CorLibTypeSignature or GenericInstanceTypeSignature) + { + var parameterType = parameter.ParameterType.Resolve() + ?? throw new InvalidOperationException($"Couldn't resolve parameter type '{parameter.ParameterType}'"); + + isByRefLike = parameterType.IsByRefLike; + } + else + { + isByRefLike = false; + } + + return refKind is RefKind.None && isByRefLike && scopedKind == ScopedKind.ScopedValue; + } + + private static ScopedKind GetScopedKind(this Parameter parameter, RefKind refKind) + { + var definition = parameter.GetOrCreateDefinition(); + + if (definition.HasCustomAttribute("System.Diagnostics.CodeAnalysis", "UnscopedRefAttribute")) + { + return ScopedKind.None; + } + + if (definition.HasCustomAttribute("System.Runtime.CompilerServices", "ScopedRefAttribute")) + { + if (parameter.ParameterType.StripModifiers() is ByReferenceTypeSignature) + { + return ScopedKind.ScopedRef; + } + + return ScopedKind.ScopedValue; + } + + if ( /* TODO module.UseUpdatedEscapeRules && */ refKind == RefKind.Out) + { + return ScopedKind.ScopedRef; + } + + return ScopedKind.None; + } +} diff --git a/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Properties.cs b/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Properties.cs new file mode 100644 index 0000000..fbce653 --- /dev/null +++ b/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Properties.cs @@ -0,0 +1,86 @@ +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using CompatUnbreaker.Tool.Utilities.AsmResolver; +using CompatUnbreaker.Utilities.AsmResolver; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Editing; +using Accessibility = Microsoft.CodeAnalysis.Accessibility; + +namespace CompatUnbreaker.Tool.SkeletonGeneration.CodeGeneration; + +internal static partial class SyntaxGeneratorExtensions +{ + public static SyntaxNode PropertyDeclaration( + this SyntaxGenerator syntaxGenerator, + PropertyDefinition property, + IEnumerable? getAccessorStatements = null, + IEnumerable? setAccessorStatements = null + ) + { + /*property.IsIndexer ? syntaxGenerator.IndexerDeclaration(property) :*/ + + var typeContext = TypeContext.From(property, property); + + var propertyAccessibility = (Accessibility) property.GetAccessibility(); + var getMethodSymbol = property.GetMethod; + var setMethodSymbol = property.SetMethod; + + SyntaxNode? getAccessor = null; + SyntaxNode? setAccessor = null; + + if (getMethodSymbol is not null) + { + var getMethodAccessibility = (Accessibility) getMethodSymbol.GetAccessibility(); + getAccessor = syntaxGenerator.GetAccessorDeclaration(getMethodAccessibility < propertyAccessibility ? getMethodAccessibility : Accessibility.NotApplicable, getAccessorStatements); + } + + if (setMethodSymbol is not null) + { + var setMethodAccessibility = (Accessibility) setMethodSymbol.GetAccessibility(); + setAccessor = SetAccessorDeclaration( + syntaxGenerator, + setMethodAccessibility < propertyAccessibility ? setMethodAccessibility : Accessibility.NotApplicable, + isInitOnly: setMethodSymbol.IsInitOnly(), + setAccessorStatements); + } + + var propDecl = PropertyDeclaration( + syntaxGenerator, + property.Name, + TypeExpression(syntaxGenerator, property.Signature.ReturnType, property.GetRefKind(), typeContext), + getAccessor, + setAccessor, + propertyAccessibility, + property.GetModifiers()); + + // TODO + // if (property.ExplicitInterfaceImplementations.Length > 0) + // { + // propDecl = this.WithExplicitInterfaceImplementations(propDecl, + // ImmutableArray.CastUp(property.ExplicitInterfaceImplementations)); + // } + + return syntaxGenerator.AddAttributes(propDecl, property.CustomAttributes); + } + + private static bool IsInitOnly(this MethodDefinition method) + { + return !method.IsStatic /*&& method.PropertySet */ && + method.Signature.ReturnType?.HasCustomModifier(m => m.IsRequired && m.IsTypeOf("System.Runtime.CompilerServices", "IsExternalInit")) == true; + } + + private static RefKind GetRefKind(this PropertyDefinition property) + { + if (property.Signature.ReturnType is ByReferenceTypeSignature) + { + if (property.HasIsReadOnlyAttribute()) + { + return RefKind.RefReadOnly; + } + + return RefKind.Ref; + } + + return RefKind.None; + } +} diff --git a/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Types.cs b/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Types.cs new file mode 100644 index 0000000..f50b182 --- /dev/null +++ b/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Types.cs @@ -0,0 +1,209 @@ +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using AsmResolver.PE.DotNet.Cil; +using AsmResolver.PE.DotNet.Metadata.Tables; +using CompatUnbreaker.Tool.Utilities; +using CompatUnbreaker.Tool.Utilities.AsmResolver; +using CompatUnbreaker.Utilities.AsmResolver; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using Accessibility = Microsoft.CodeAnalysis.Accessibility; + +namespace CompatUnbreaker.Tool.SkeletonGeneration.CodeGeneration; + +internal static partial class SyntaxGeneratorExtensions +{ + public static SyntaxNode TypeDeclaration(this SyntaxGenerator syntaxGenerator, TypeDefinition type, Func? memberFilter = null) + { + ArgumentNullException.ThrowIfNull(type.Name); + + var name = MetadataHelpers.InferTypeArityAndUnmangleMetadataName(type.Name.Value, out var arity).ToString(); + + var typeContext = TypeContext.From(type, type); + + var interfaces = type.Interfaces.Select(i => + { + ArgumentNullException.ThrowIfNull(i.Interface); + return syntaxGenerator.TypeExpression(i.Interface, typeContext); + }); + + var members = type.GetMembers().Where(syntaxGenerator.CanBeDeclared); + if (memberFilter != null) members = members.Where(memberFilter); + + SyntaxNode declaration; + + if (type.IsDelegate) + { + var invoke = type.Methods.Single(m => m.Name == "Invoke"); + ArgumentNullException.ThrowIfNull(invoke.Signature); + + typeContext = TypeContext.From(invoke, invoke); + var returnType = invoke.Signature.ReturnType; + + declaration = syntaxGenerator.DelegateDeclaration( + name, + typeParameters: null, + parameters: invoke.Parameters.Select(p => syntaxGenerator.ParameterDeclaration(p)), + returnType: returnType.ElementType == ElementType.Void ? null : syntaxGenerator.TypeExpression(returnType, typeContext), + accessibility: (Accessibility) type.GetAccessibility(), + modifiers: type.GetModifiers() + ); + } + else if (type.IsEnum) + { + var underlyingType = type.GetEnumUnderlyingType(); + + declaration = syntaxGenerator.EnumDeclaration( + name, + underlyingType: underlyingType is null or { ElementType: ElementType.I4 } + ? null + : syntaxGenerator.TypeExpression(underlyingType, typeContext), + accessibility: (Accessibility) type.GetAccessibility(), + members: type.Fields.Where(f => f.Name != "value__").Select(m => syntaxGenerator.Declaration(m, memberFilter)) + ); + } + else if (type.IsValueType) + { + declaration = syntaxGenerator.StructDeclaration( + type.IsRecord(), + name, + typeParameters: null, + accessibility: (Accessibility) type.GetAccessibility(), + modifiers: type.GetModifiers(), + interfaceTypes: interfaces, + members: members.Select(m => syntaxGenerator.Declaration(m, memberFilter)) + ); + } + else if (type.IsInterface) + { + declaration = syntaxGenerator.InterfaceDeclaration( + name, + typeParameters: null, + accessibility: (Accessibility) type.GetAccessibility(), + interfaceTypes: interfaces, + members: members.Select(m => syntaxGenerator.Declaration(m, memberFilter)) + ); + } + else if (type.IsClass) + { + if (type.GetSynthesizedConstructor() is { } synthesizedConstructor) + { + members = members.Where(m => m != synthesizedConstructor); + } + + declaration = syntaxGenerator.ClassDeclaration( + type.IsRecord(), + name, + typeParameters: null, + accessibility: (Accessibility) type.GetAccessibility(), + modifiers: type.GetModifiers(), + baseType: type.BaseType != null && type.BaseType.ToTypeSignature().ElementType != ElementType.Object ? syntaxGenerator.TypeExpression(type.BaseType, typeContext) : null, + interfaceTypes: interfaces, + members: members.Select(m => syntaxGenerator.Declaration(m, memberFilter)).Select(d => d.WithLeadingTrivia(SyntaxFactory.LineFeed)) + ); + } + else + { + throw new ArgumentOutOfRangeException(nameof(type)); + } + + declaration = syntaxGenerator.WithTypeParametersAndConstraints(declaration, type.GenericParameters.TakeLast(arity).ToArray(), typeContext); + declaration = syntaxGenerator.AddAttributes(declaration, type.CustomAttributes); + + if (declaration is TypeDeclarationSyntax { Members.Count: 0 } or EnumDeclarationSyntax { Members.Count: 0 }) + { + declaration = ((BaseTypeDeclarationSyntax) declaration) + .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)) + .WithOpenBraceToken(default) + .WithCloseBraceToken(default); + } + + return declaration; + } + + public static bool CanBeDeclared(this SyntaxGenerator syntaxGenerator, IMemberDefinition member) + { + ArgumentNullException.ThrowIfNull(member.Name); + + if (member is IHasCustomAttribute hasCustomAttribute && hasCustomAttribute.IsCompilerGenerated()) + { + return false; + } + + if (member is MethodDefinition method) + { + if (method.IsConstructor || method.IsDestructor()) + { + return true; + } + + if (method.IsPropertyAccessor() || method.IsEventAccessor()) + { + return false; + } + } + + var name = MetadataHelpers.InferTypeArityAndUnmangleMetadataName(member.Name, out _).ToString(); + return SyntaxFacts.IsValidIdentifier(name); + } + + private static MethodDefinition? GetSynthesizedConstructor(this TypeDefinition type) + { + if (type.IsStatic() || type.IsInterface) + return null; + + var isRecord = type.IsRecord(); + + MethodDefinition? constructor = null; + + foreach (var method in type.Methods) + { + if (method is not { IsStatic: false, IsConstructor: true }) + continue; + + // Ignore the record copy constructor + if (isRecord && + method.Parameters.Count == 1 && + method.GenericParameters.Count == 0 && + SignatureComparer.Default.Equals(method.Parameters[0].ParameterType, type.ToTypeSignature())) + { + continue; + } + + if (constructor != null) + { + return null; + } + + constructor = method; + } + + if (constructor == null) + return null; + + if (constructor.Parameters.Count != 0 || + (constructor.Attributes & MethodAttributes.MemberAccessMask) != (type.IsAbstract ? MethodAttributes.Family : MethodAttributes.Public) || + constructor.CustomAttributes.Any(a => !IsReserved(a))) + { + return null; + } + + if (constructor.CilMethodBody is not { } methodBody || + !methodBody.Instructions.Match( + i => i.OpCode == CilOpCodes.Ldarg_0, + i => i.OpCode == CilOpCodes.Call && + i.Operand is IMethodDescriptor method && + method.DeclaringType == type.BaseType && + method.Name == constructor.Name && + method.Signature is { HasThis: true, ParameterTypes.Count: 0 }, + i => i.OpCode == CilOpCodes.Ret + )) + { + return null; + } + + return constructor; + } +} diff --git a/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.UnsafeAccessors.cs b/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.UnsafeAccessors.cs new file mode 100644 index 0000000..1c94856 --- /dev/null +++ b/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.UnsafeAccessors.cs @@ -0,0 +1,115 @@ +using System.Runtime.CompilerServices; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Editing; + +namespace CompatUnbreaker.Tool.SkeletonGeneration.CodeGeneration; + +internal static partial class SyntaxGeneratorExtensions +{ + [UnsafeAccessor(UnsafeAccessorKind.Method)] + private static extern SyntaxNode ClassDeclaration( + this SyntaxGenerator @this, + bool isRecord, + string name, + IEnumerable? typeParameters, + Accessibility accessibility, + DeclarationModifiers modifiers, + SyntaxNode? baseType, + IEnumerable? interfaceTypes, + IEnumerable? members + ); + + [UnsafeAccessor(UnsafeAccessorKind.Method)] + private static extern SyntaxNode StructDeclaration( + this SyntaxGenerator @this, + bool isRecord, + string name, + IEnumerable? typeParameters, + Accessibility accessibility, + DeclarationModifiers modifiers, + IEnumerable? interfaceTypes, + IEnumerable? members + ); + + [UnsafeAccessor(UnsafeAccessorKind.Method)] + private static extern SyntaxNode EnumDeclaration( + this SyntaxGenerator @this, + string name, + SyntaxNode? underlyingType, + Accessibility accessibility = Accessibility.NotApplicable, + DeclarationModifiers modifiers = default, + IEnumerable? members = null + ); + + [UnsafeAccessor(UnsafeAccessorKind.Method)] + private static extern SyntaxNode MethodDeclaration( + this SyntaxGenerator @this, + string name, + IEnumerable? parameters, + IEnumerable? typeParameters, + SyntaxNode? returnType, + Accessibility accessibility, + DeclarationModifiers modifiers, + IEnumerable? statements + ); + + [UnsafeAccessor(UnsafeAccessorKind.Method)] + private static extern SyntaxNode ParameterDeclaration( + this SyntaxGenerator @this, + string name, + SyntaxNode? type, + SyntaxNode? initializer, + RefKind refKind, + bool isExtension, + bool isParams, + bool isScoped + ); + + [UnsafeAccessor(UnsafeAccessorKind.Method)] + private static extern SyntaxNode OperatorDeclaration( + this SyntaxGenerator @this, + string operatorName, + bool isImplicitConversion, + IEnumerable? parameters = null, + SyntaxNode? returnType = null, + Accessibility accessibility = Accessibility.NotApplicable, + DeclarationModifiers modifiers = default, + IEnumerable? statements = null + ); + + [UnsafeAccessor(UnsafeAccessorKind.Method)] + private static extern SyntaxNode PropertyDeclaration( + this SyntaxGenerator @this, + string name, + SyntaxNode type, + SyntaxNode? getAccessor, + SyntaxNode? setAccessor, + Accessibility accessibility, + DeclarationModifiers modifiers + ); + + [UnsafeAccessor(UnsafeAccessorKind.Method)] + private static extern SyntaxNode SetAccessorDeclaration( + this SyntaxGenerator @this, + Accessibility accessibility, + bool isInitOnly, + IEnumerable? statements + ); + + [UnsafeAccessor(UnsafeAccessorKind.Method)] + private static extern SyntaxNode WithTypeParameters( + this SyntaxGenerator @this, + SyntaxNode declaration, + IEnumerable typeParameters + ); + + [UnsafeAccessor(UnsafeAccessorKind.Method)] + private static extern SyntaxNode WithTypeConstraint( + this SyntaxGenerator @this, + SyntaxNode declaration, + string typeParameterName, + SpecialTypeConstraintKind kinds, + bool isUnamangedType, + IEnumerable? types + ); +} diff --git a/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.cs b/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.cs new file mode 100644 index 0000000..f2fa19a --- /dev/null +++ b/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.cs @@ -0,0 +1,44 @@ +using AsmResolver.DotNet; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; + +namespace CompatUnbreaker.Tool.SkeletonGeneration.CodeGeneration; + +internal static partial class SyntaxGeneratorExtensions +{ + public static TypeSyntax TypeExpression(this SyntaxGenerator syntaxGenerator, ITypeDescriptor typeDescriptor, TypeContext context) + { + return AsmResolverTypeSyntaxGenerator.TypeExpression(typeDescriptor, context); + } + + private static TypeSyntax TypeExpression(this SyntaxGenerator syntaxGenerator, ITypeDescriptor typeSymbol, RefKind refKind, TypeContext context) + { + var type = syntaxGenerator.TypeExpression(typeSymbol, context); + if (type is RefTypeSyntax refType) + { + type = refType.Type; + } + + return refKind switch + { + RefKind.Ref => SyntaxFactory.RefType(type), + RefKind.RefReadOnly => SyntaxFactory.RefType(SyntaxFactory.Token(SyntaxKind.RefKeyword), SyntaxFactory.Token(SyntaxKind.ReadOnlyKeyword), type), + _ => type, + }; + } + + public static SyntaxNode Declaration(this SyntaxGenerator syntaxGenerator, IMemberDefinition member, Func? memberFilter = null) + { + return member switch + { + TypeDefinition type => syntaxGenerator.TypeDeclaration(type, memberFilter), + MethodDefinition method => syntaxGenerator.MethodDeclaration(method), + FieldDefinition field => syntaxGenerator.FieldDeclaration(field), + PropertyDefinition property => syntaxGenerator.PropertyDeclaration(property), + EventDefinition @event => syntaxGenerator.EventDeclaration(@event), + _ => throw new ArgumentOutOfRangeException(nameof(member)), + }; + } +} diff --git a/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/TypeContext.cs b/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/TypeContext.cs new file mode 100644 index 0000000..b15e3ee --- /dev/null +++ b/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/TypeContext.cs @@ -0,0 +1,150 @@ +using AsmResolver; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; + +namespace CompatUnbreaker.Tool.SkeletonGeneration.CodeGeneration; + +internal record TypeContext( + TypeContext.GenericContext Generic, + TypeContext.TransformContext Transform +) +{ + public static TypeContext Empty { get; } = new(GenericContext.Empty, TransformContext.Empty); + + public sealed record GenericContext(TypeDefinition? Type, MethodDefinition? Method) + { + public static GenericContext Empty { get; } = new(null, null); + + public GenericParameter? GetGenericParameter(GenericParameterSignature parameter) + { + var genericParameters = parameter.ParameterType switch + { + GenericParameterType.Type => Type?.GenericParameters, + GenericParameterType.Method => Method?.GenericParameters, + _ => throw new ArgumentOutOfRangeException(nameof(parameter)), + }; + + if (genericParameters is null) + return null; + + if (parameter.Index >= 0 && parameter.Index < genericParameters.Count) + return genericParameters[parameter.Index]; + + return null; + } + } + + public sealed record TransformContext( + NullableAnnotation? DefaultNullableTransform = null, + NullableAnnotation[]? NullableTransforms = null, + bool[]? DynamicTransforms = null, + string?[]? TupleElementNames = null + ) + { + public static TransformContext Empty { get; } = new(); + + private int _nullablePosition; + private int _dynamicPosition; + private int _namesPosition; + + public NullableAnnotation? TryConsumeNullableTransform() + { + return NullableTransforms?[_nullablePosition++] ?? DefaultNullableTransform; + } + + public bool? TryConsumeDynamicTransform() + { + return DynamicTransforms?[_dynamicPosition++]; + } + + public Span ConsumeTupleElementNames(int numberOfElements) + { + if (TupleElementNames == null) return default; + + var result = TupleElementNames.AsSpan(_namesPosition, numberOfElements); + _namesPosition += numberOfElements; + return result; + } + + public static TransformContext From(NullableAnnotation? defaultNullableTransform, IHasCustomAttribute attributeProvider) + { + var nullableAttribute = attributeProvider.FindCustomAttributes("System.Runtime.CompilerServices", "NullableAttribute").SingleOrDefault(); + var nullableTransforms = nullableAttribute?.Signature!.FixedArguments.Single().Elements.Cast().Cast().ToArray(); + + var dynamicTransformsAttribute = attributeProvider.FindCustomAttributes("System.Runtime.CompilerServices", "DynamicAttribute").SingleOrDefault(); + bool[]? dynamicTransforms; + if (dynamicTransformsAttribute != null) + { + var arguments = dynamicTransformsAttribute.Signature!.FixedArguments; + dynamicTransforms = arguments.Count == 0 ? [true] : arguments.Single().Elements.Cast().ToArray(); + } + else + { + dynamicTransforms = null; + } + + var tupleElementNamesAttribute = attributeProvider.FindCustomAttributes("System.Runtime.CompilerServices", "TupleElementNamesAttribute").SingleOrDefault(); + var tupleElementNames = tupleElementNamesAttribute?.Signature!.FixedArguments.Single().Elements.Cast().Select(s => s?.Value).ToArray(); + + return new TransformContext( + defaultNullableTransform, + nullableTransforms, + dynamicTransforms, + tupleElementNames + ); + } + } + + public TypeContext WithTransformsAttributeProvider(IHasCustomAttribute attributeProvider) + { + return this with { Transform = TransformContext.From(Transform.DefaultNullableTransform, attributeProvider) }; + } + + public static TypeContext From(TypeDefinition type, IHasCustomAttribute? transformsAttributeProvider) + { + return From(type, null, transformsAttributeProvider); + } + + public static TypeContext From(MethodDefinition method, IHasCustomAttribute? transformsAttributeProvider) + { + return From(method.DeclaringType, method, transformsAttributeProvider); + } + + public static TypeContext From(IMemberDefinition member, IHasCustomAttribute? transformsAttributeProvider) + { + if (member is MethodDefinition method) return From(method, transformsAttributeProvider); + return From(member.DeclaringType, null, transformsAttributeProvider); + } + + private static TypeContext From(TypeDefinition? type, MethodDefinition? method, IHasCustomAttribute? transformsAttributeProvider) + { + var defaultNullableTransform = GetDefaultNullableTransform((IMemberDefinition?) method ?? type); + + return new TypeContext( + new GenericContext(type, method), + transformsAttributeProvider != null + ? TransformContext.From(defaultNullableTransform, transformsAttributeProvider) + : new TransformContext(defaultNullableTransform) + ); + } + + private static NullableAnnotation? GetDefaultNullableTransform(IMemberDefinition? member) + { + if (member == null) return null; + + var nullableContextAttribute = ((IHasCustomAttribute) member).FindCustomAttributes("System.Runtime.CompilerServices", "NullableContextAttribute").SingleOrDefault(); + + if (nullableContextAttribute != null) + { + ArgumentNullException.ThrowIfNull(nullableContextAttribute.Signature); + return (NullableAnnotation) (byte) nullableContextAttribute.Signature.FixedArguments.Single().Element!; + } + + if (member.DeclaringType != null) + { + return GetDefaultNullableTransform(member.DeclaringType); + } + + return null; + } +} diff --git a/CompatUnbreaker.Tool/SkeletonGeneration/MethodBodyRewriter.cs b/CompatUnbreaker.Tool/SkeletonGeneration/MethodBodyRewriter.cs new file mode 100644 index 0000000..a560634 --- /dev/null +++ b/CompatUnbreaker.Tool/SkeletonGeneration/MethodBodyRewriter.cs @@ -0,0 +1,50 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace CompatUnbreaker.Tool.SkeletonGeneration; + +internal sealed class MethodBodyRewriter : CSharpSyntaxRewriter +{ + public static MethodBodyRewriter Instance { get; } = new(); + + public static BlockSyntax Body { get; } = SyntaxFactory.Block(SyntaxFactory.ParseStatement("throw new NotImplementedException();")); + public static ArrowExpressionClauseSyntax ExpressionBody { get; } = SyntaxFactory.ArrowExpressionClause(SyntaxFactory.ParseExpression("throw new NotImplementedException()")); + + [return: NotNullIfNotNull(nameof(node))] + public override SyntaxNode? Visit(SyntaxNode? node) + { + if (node is BaseMethodDeclarationSyntax methodNode) + { + node = VisitBaseMethodDeclarationSyntax(methodNode); + } + + return base.Visit(node); + } + + private SyntaxNode VisitBaseMethodDeclarationSyntax(BaseMethodDeclarationSyntax node) + { + if (node.Body != null) + { + return node.WithBody(Body); + } + + return node; + } + + public override SyntaxNode VisitAccessorDeclaration(AccessorDeclarationSyntax node) + { + // if (node.Parent?.Parent is BasePropertyDeclarationSyntax property + // && (property.Modifiers.Any(SyntaxKind.AbstractKeyword) || property.Modifiers.Any(SyntaxKind.ExternKeyword))) + // { + // return node; + // } + if (node.Body == null && node.ExpressionBody == null) + { + return node; + } + + return node.WithBody(null).WithExpressionBody(ExpressionBody).WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)); + } +} diff --git a/CompatUnbreaker.Tool/SkeletonGeneration/SkeletonGenerator.cs b/CompatUnbreaker.Tool/SkeletonGeneration/SkeletonGenerator.cs new file mode 100644 index 0000000..b12a79f --- /dev/null +++ b/CompatUnbreaker.Tool/SkeletonGeneration/SkeletonGenerator.cs @@ -0,0 +1,632 @@ +using System.Diagnostics; +using System.Reflection; +using AsmResolver; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using CompatUnbreaker.Tool.ApiCompatibility.AssemblyMapping; +using CompatUnbreaker.Tool.ApiCompatibility.Comparing; +using CompatUnbreaker.Tool.ApiCompatibility.Comparing.Rules; +using CompatUnbreaker.Tool.SkeletonGeneration.CodeGeneration; +using CompatUnbreaker.Tool.Utilities; +using CompatUnbreaker.Tool.Utilities.AsmResolver; +using CompatUnbreaker.Tool.Utilities.Roslyn; +using CompatUnbreaker.Utilities.AsmResolver; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.MSBuild; +using Microsoft.CodeAnalysis.Simplification; +using MonoMod.RuntimeDetour; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace CompatUnbreaker.Tool.SkeletonGeneration; + +internal static class SkeletonGenerator +{ + public static async Task<(MSBuildWorkspace Workspace, Project Project)> GenerateAsync(AssemblyMapper assemblyMapper, string projectPath) + { + var workspace = MSBuildWorkspace.Create(); + var project = await workspace.OpenProjectAsync(projectPath); + + return (workspace, await GenerateAsync(assemblyMapper, workspace, project)); + } + + public static async Task GenerateAsync(AssemblyMapper assemblyMapper, Workspace workspace, Project project) + { + using var hook = new Hook( + Type.GetType("Microsoft.CodeAnalysis.CSharp.Extensions.SemanticModelExtensions, Microsoft.CodeAnalysis.CSharp.Workspaces")! + .GetMethod("UnifiesNativeIntegers", BindingFlags.Public | BindingFlags.Static)!, + bool (SemanticModel _) => false + ); + + var compilation = await project.GetCompilationAsync() ?? throw new InvalidOperationException("Failed to get compilation"); + var shimTypes = compilation.Assembly.GlobalNamespace.GetAllTypes().ToArray(); + var shimExtensionTypes = shimTypes.Where(t => + t.ContainingType == null && + t.GetAttributes().Any(a => a.AttributeClass?.GetFullMetadataName() == "CompatUnbreaker.Attributes.UnbreakerExtensionsAttribute") + ).SelectMany(t => t.GetTypeMembers()).ToArray(); + + var topLevelTypeDifferences = new Dictionary(); + var memberDifferences = new Dictionary>(); + + void AddMemberDifference(TypeMapper typeMapper, CompatDifference difference) + { + if (!memberDifferences.TryGetValue(typeMapper, out var list)) + { + memberDifferences.Add(typeMapper, list = []); + } + + list.Add(difference); + } + + var apiComparer = new ApiComparer(); + apiComparer.Compare(assemblyMapper); + + foreach (var difference in apiComparer.CompatDifferences) + { + Console.WriteLine(difference); + + switch (difference) + { + case TypeMustExistDifference typeDifference: + { + if (typeDifference.Mapper.DeclaringType is { } declaringType) + { + AddMemberDifference(declaringType, typeDifference); + break; + } + + topLevelTypeDifferences.Add(typeDifference.Mapper, typeDifference); + break; + } + + case MemberMustExistDifference memberDifference: + { + var typeMapper = memberDifference.Mapper.DeclaringType; + AddMemberDifference(typeMapper, memberDifference); + break; + } + + default: + break; + } + } + + Document AddDocument(TypeDefinition type, SyntaxNode syntaxRoot) + { + var name = type.Name + ".cs"; + + var folders = type.Namespace is null + ? [] + : type.Namespace.Value.TrimPrefix(project.DefaultNamespace).Split('.', StringSplitOptions.RemoveEmptyEntries); + + if (project.Documents.Any(d => d.Folders.SequenceEqual(folders) && d.Name == name)) + { + throw new InvalidOperationException($"Duplicate document '{(folders.Length == 0 ? string.Empty : string.Join('/', folders))}{name}'"); + } + + return project.AddDocument(name, syntaxRoot, folders); + } + + var modifiedDocuments = new HashSet(); + + foreach (var (mapper, difference) in topLevelTypeDifferences) + { + if (difference is not TypeMustExistDifference) continue; + + var (leftType, rightType) = mapper; + if (leftType == null || rightType != null) continue; + + var syntaxGenerator = SyntaxGenerator.GetGenerator(project); + + var typeDeclaration = CreateDeclaration(syntaxGenerator, leftType); + + var syntaxRoot = CreateCompilationUnit(syntaxGenerator, leftType.Namespace, typeDeclaration); + + syntaxRoot = (CompilationUnitSyntax) MethodBodyRewriter.Instance.Visit(syntaxRoot); + + var document = AddDocument(leftType, syntaxRoot); + modifiedDocuments.Add(document.Id); + project = document.Project; + } + + foreach (var (typeMapper, differences) in memberDifferences) + { + var (leftType, rightType) = typeMapper; + if (leftType == null) continue; + + var missingMembers = new List(); + + foreach (var difference in differences) + { + IMemberDefinition? left; + IMemberDefinition? right; + + switch (difference) + { + case TypeMustExistDifference typeMustExistDifference: + (left, right) = typeMustExistDifference.Mapper; + break; + + case MemberMustExistDifference memberMustExistDifference: + (left, right) = memberMustExistDifference.Mapper; + break; + + default: + throw new UnreachableException(); + } + + if (left == null || right != null) continue; + + missingMembers.Add(left); + } + + var syntaxGenerator = SyntaxGenerator.GetGenerator(project); + + ITypeSymbol? existingShimType = compilation.Assembly.GetTypeByMetadataName(leftType.FullName); + existingShimType ??= shimTypes.SingleOrDefault(t => + t.GetAttributes().Any(a => + a.AttributeClass?.GetFullMetadataName() == "CompatUnbreaker.Attributes.UnbreakerReplaceAttribute" && + ((ITypeSymbol) a.ConstructorArguments.Single().Value!).GetFullMetadataName() == leftType.FullName + ) + ); + + if (existingShimType != null) + { + var syntaxReference = existingShimType.DeclaringSyntaxReferences.First(); + var document = project.GetDocument(syntaxReference.SyntaxTree); + var syntaxNode = await syntaxReference.GetSyntaxAsync(); + + var editor = await DocumentEditor.CreateAsync(document); + + // TODO insert members preserving member kind order + foreach (var member in missingMembers) + { + if (!syntaxGenerator.CanBeDeclared(member)) continue; + editor.AddMember(syntaxNode, CreateDeclaration(syntaxGenerator, member)); + } + + document = editor.GetChangedDocument(); + modifiedDocuments.Add(document.Id); + project = document.Project; + } + else + { + var existingShimNativeExtensionType = shimExtensionTypes.FirstOrDefault(t => + t.ExtensionParameter != null && + t.ExtensionParameter.Type.GetFullMetadataName() == leftType.FullName + )?.DeclaringSyntaxReferences.First(); + + var existingShimUnbreakerExtensionType = shimExtensionTypes.FirstOrDefault(t => + t.GetAttributes().Any(a => + a.AttributeClass?.GetFullMetadataName() == "CompatUnbreaker.Attributes.UnbreakerExtensionAttribute" && + ((ITypeSymbol) a.ConstructorArguments.Single().Value!).GetFullMetadataName() == leftType.FullName + ) + )?.DeclaringSyntaxReferences.First(); + + Document? document = null; + SyntaxNode? extensionsType = null; + + if (existingShimNativeExtensionType != null || existingShimUnbreakerExtensionType != null) + { + document = project.GetDocument((existingShimNativeExtensionType ?? existingShimUnbreakerExtensionType)!.SyntaxTree); + extensionsType = (await (existingShimNativeExtensionType ?? existingShimUnbreakerExtensionType)!.GetSyntaxAsync()).Parent; + } + + var (nativeMembers, unbreakerMembers) = CreateExtensionMembers(syntaxGenerator, missingMembers); + + if (nativeMembers.Count > 0) + { + await EditOrAddDocumentAsync(nativeMembers, existingShimNativeExtensionType, members => + { + return ExtensionDeclaration() + .AddParameterListParameters( + Parameter(leftType.IsStatic() ? default : "this".ToIdentifierToken()) + .WithType(syntaxGenerator.TypeExpression(leftType, TypeContext.Empty)) + ) + .WithOpenBraceToken(Token(SyntaxKind.OpenBraceToken)) + .WithCloseBraceToken(Token(SyntaxKind.CloseBraceToken)) + .WithMembers( + List(members) + ); + }); + } + + if (unbreakerMembers.Count > 0) + { + await EditOrAddDocumentAsync(unbreakerMembers, existingShimUnbreakerExtensionType, members => + { + MemberDeclarationSyntax node = ClassDeclaration($"{leftType.GetUnmangledName()}Extension") + .AddAttributeLists( + (AttributeListSyntax) syntaxGenerator.Attribute( + "UnbreakerExtension", + TypeOfExpression(syntaxGenerator.TypeExpression(leftType, TypeContext.Empty)) + ) + ) + .WithModifiers( + TokenList(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.StaticKeyword)) + ) + .WithMembers( + List(members) + ); + + node = (MemberDeclarationSyntax) syntaxGenerator.WithTypeParametersAndConstraints(node, leftType.GenericParameters, TypeContext.From(leftType, leftType)); + + return node; + }); + } + + async Task EditOrAddDocumentAsync(List members, SyntaxReference? extensionType, Func, MemberDeclarationSyntax> factory) + { + if (extensionType != null) + { + var syntaxNode = await extensionType.GetSyntaxAsync(); + + var editor = await DocumentEditor.CreateAsync(document); + + // TODO insert members preserving member kind order + foreach (var member in members) + { + editor.AddMember(syntaxNode, member.WithSimplifyAnnotations()); + } + + document = editor.GetChangedDocument(); + } + else if (document != null) + { + Debug.Assert(extensionsType != null); + + var editor = await DocumentEditor.CreateAsync(document); + + editor.AddMember(extensionsType, factory(members)); + + document = editor.GetChangedDocument(); + } + else + { + if (typeMapper.DeclaringType != null) + { + throw new NotImplementedException(); + } + + MemberDeclarationSyntax node = ClassDeclaration($"{leftType.GetUnmangledName()}Extensions") + .AddAttributeLists( + (AttributeListSyntax) syntaxGenerator.Attribute("UnbreakerExtensions") + ) + .WithModifiers( + TokenList(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.StaticKeyword)) + ) + .WithMembers( + List([factory(members)]) + ); + + var syntaxRoot = CreateCompilationUnit(syntaxGenerator, leftType.Namespace, node); + + document = AddDocument(leftType, syntaxRoot); + extensionsType = (await document.GetSyntaxRootAsync())!.DescendantNodes().OfType().First(); + } + + modifiedDocuments.Add(document.Id); + project = document.Project; + } + } + } + + foreach (var documentId in modifiedDocuments) + { + var document = project.GetDocument(documentId)!; + + document = await SimplifyAsync(document); + + Console.WriteLine((await document.GetTextAsync()).ToString()); + project = document.Project; + } + + Console.Write("Enter `y` to save: "); + if (Console.ReadKey().KeyChar == 'y') + { + // TODO stupid + var projectText = await File.ReadAllTextAsync(project.FilePath); + + if (!workspace.TryApplyChanges(project.Solution)) + { + Console.WriteLine("Failed to apply changes"); + } + + await File.WriteAllTextAsync(project.FilePath, projectText); + } + + return project; + } + + private static async Task SimplifyAsync(Document document) + { + document = await ImportAdder.AddImportsAsync(document, Simplifier.AddImportsAnnotation); + + // ImportAdder adds leading trivia before the first member, but it's not cleaned up when Simplifier removes all usings, so just let Formatter handle it + { + var syntaxRoot = (CompilationUnitSyntax) (await document.GetSyntaxRootAsync())!; + var firstNode = syntaxRoot.Members.First(); + document = document.WithSyntaxRoot(syntaxRoot.ReplaceNode(firstNode, firstNode.WithoutLeadingTrivia())); + } + + document = await Simplifier.ReduceAsync(document, Simplifier.Annotation); + document = await Formatter.FormatAsync(document, Formatter.Annotation); + document = await Formatter.FormatAsync(document, SyntaxAnnotation.ElasticAnnotation); + + return document; + } + + private static MemberDeclarationSyntax CreateDeclaration(SyntaxGenerator syntaxGenerator, IMemberDefinition member) + { + var declaration = syntaxGenerator.Declaration(member, m => m.IsVisibleOutsideOfAssembly()).WithSimplifyAnnotations(); + declaration = MethodBodyRewriter.Instance.Visit(declaration); + return (MemberDeclarationSyntax) declaration; + } + + private static CompilationUnitSyntax CreateCompilationUnit(SyntaxGenerator syntaxGenerator, Utf8String? @namespace, MemberDeclarationSyntax node) + { + if (@namespace is not null) + { + var namespaceSyntax = (NameSyntax) syntaxGenerator.DottedName(@namespace); + node = FileScopedNamespaceDeclaration(namespaceSyntax) + .AddMembers(node); + } + + return CompilationUnit() + .AddMembers(node) + .WithTrailingTrivia(LineFeed) + .WithSimplifyAnnotations(); + } + + private static TNode WithSimplifyAnnotations(this TNode node) where TNode : SyntaxNode + { + return node.WithAdditionalAnnotations(Simplifier.Annotation, Simplifier.AddImportsAnnotation, Formatter.Annotation); + } + + private static (List NativeMembers, List UnbreakerMembers) CreateExtensionMembers(SyntaxGenerator syntaxGenerator, List members) + { + var nativeMembers = new List(); + var unbreakerMembers = new List(); + + foreach (var member in members) + { + if (member is TypeDefinition type) + { + unbreakerMembers.Add(CreateDeclaration(syntaxGenerator, type)); + } + else if (member is FieldDefinition field) + { + var fieldType = syntaxGenerator.TypeExpression(field.Signature!.FieldType, TypeContext.From(field, field)).ToString(); + var fieldName = field.Name!.Value.EscapeIdentifier(); + + var body = field.IsInitOnly + ? " => throw new NotImplementedException();" + : """ + + { + get => throw new NotImplementedException(); + set => throw new NotImplementedException(); + } + """; + + nativeMembers.Add(ParseMemberDeclaration( + $""" + [UnbreakerField] + public {(field.IsStatic ? "static " : string.Empty)}{fieldType} {fieldName}{body} + """ + )!.WithLeadingTrivia(LineFeed).WithTrailingTrivia(LineFeed).WithSimplifyAnnotations()); + } + else if (member is MethodDefinition method) + { + if (method.IsPropertyAccessor()) + { + continue; + } + + if (method.IsEventAccessor() && member.IsStatic()) + { + continue; + } + + if (method.IsConstructor) + { + method.Name = "Create"; + method.IsStatic = true; + method.Signature = MethodSignature.CreateStatic(method.DeclaringType.ToTypeSignature(), method.Signature.ParameterTypes); + + var declaration = (BaseMethodDeclarationSyntax) CreateDeclaration(syntaxGenerator, method); + + declaration = declaration.WithAttributeLists(declaration.AttributeLists.Insert(0, AttributeList([Attribute(IdentifierName("UnbreakerConstructor"))]))); + + unbreakerMembers.Add(declaration); + } + else + { + var declaration = (BaseMethodDeclarationSyntax) CreateDeclaration(syntaxGenerator, method); + + if (method.IsExtension()) + { + var firstParameter = declaration.ParameterList.Parameters[0]; + declaration = declaration.WithParameterList(declaration.ParameterList.WithParameters( + declaration.ParameterList.Parameters.Replace( + firstParameter, + firstParameter + .WithModifiers(firstParameter.Modifiers.Remove(firstParameter.Modifiers.Single(m => m.IsKind(SyntaxKind.ThisKeyword)))) + .WithAttributeLists(firstParameter.AttributeLists.Insert(0, AttributeList([Attribute(IdentifierName("UnbreakerThis"))]))) + ) + )); + } + + nativeMembers.Add(declaration); + } + } + else if (member is PropertyDefinition property) + { + nativeMembers.Add(CreateDeclaration(syntaxGenerator, property)); + } + else if (member is EventDefinition @event) + { + if (!@event.IsStatic()) + { + continue; // TODO + throw new NotImplementedException(); + } + + unbreakerMembers.Add(CreateDeclaration(syntaxGenerator, @event)); + } + else + { + throw new UnreachableException(); + } + } + + return (nativeMembers, unbreakerMembers); + } + + // private static MemberDeclarationSyntax CreateMemberFromClassDeclaration( + // SyntaxGenerator syntaxGenerator, + // T member + // ) where T : IMemberDefinition, IHasCustomAttribute + // { + // if (member.IsStatic()) + // { + // throw new NotImplementedException(); + // } + // + // var targetType = syntaxGenerator.TypeExpression(GetParameterType(member.DeclaringType!), TypeContext.Empty); + // var propertyType = syntaxGenerator.TypeExpression( + // member switch + // { + // PropertyDefinition property => property.Signature!.ReturnType, + // FieldDefinition field => field.Signature!.FieldType, + // _ => throw new ArgumentOutOfRangeException(nameof(member), member, null), + // }, + // TypeContext.From(member, member) + // ); + // + // var hasGetMethod = member switch + // { + // PropertyDefinition property => property.GetMethod != null, + // FieldDefinition => true, + // _ => throw new ArgumentOutOfRangeException(nameof(member), member, null), + // }; + // + // var hasSetMethod = member switch + // { + // PropertyDefinition property => property.SetMethod != null, + // FieldDefinition field => !field.IsInitOnly, + // _ => throw new ArgumentOutOfRangeException(nameof(member), member, null), + // }; + // + // var accessors = new List(); + // + // var thisParameter = syntaxGenerator.ParameterDeclaration("@this", targetType); + // + // if (hasGetMethod) + // { + // accessors.Add(syntaxGenerator.MethodDeclaration( + // accessibility: Accessibility.Public, + // modifiers: DeclarationModifiers.Static, + // returnType: propertyType, + // name: "Get", + // parameters: [thisParameter] + // )); + // } + // + // if (hasSetMethod) + // { + // accessors.Add(syntaxGenerator.MethodDeclaration( + // accessibility: Accessibility.Public, + // modifiers: DeclarationModifiers.Static, + // name: "Set", + // parameters: + // [ + // thisParameter, + // syntaxGenerator.ParameterDeclaration("value", propertyType), + // ] + // )); + // } + // + // var declaration = syntaxGenerator.ClassDeclaration( + // member.Name!.EscapeIdentifier(), + // accessibility: Accessibility.Public, + // modifiers: DeclarationModifiers.Static, + // members: accessors + // ); + // + // declaration = declaration.ReplaceNodes( + // declaration.DescendantNodes().OfType(), + // (node, _) => node + // .WithBody(null) + // .WithExpressionBody(MethodBodyRewriter.ExpressionBody) + // .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) + // ); + // + // return (MemberDeclarationSyntax) syntaxGenerator.AddAttributes( + // declaration, + // syntaxGenerator.Attribute(member switch + // { + // PropertyDefinition => "CompatUnbreaker.Attributes.UnbreakerPropertyAttribute", + // FieldDefinition => "CompatUnbreaker.Attributes.UnbreakerFieldAttribute", + // EventDefinition => "CompatUnbreaker.Attributes.UnbreakerEventAttribute", + // _ => throw new ArgumentOutOfRangeException(nameof(member), member, null), + // }) + // ); + // } + + // private static void ConvertInstanceMethodToExtension(MethodDefinition method) + // { + // if (method.IsStatic) return; + // + // ArgumentNullException.ThrowIfNull(method.DeclaringType); + // ArgumentNullException.ThrowIfNull(method.Signature); + // + // var module = method.Module; + // ArgumentNullException.ThrowIfNull(module); + // + // method.CustomAttributes.Add(new CustomAttribute( + // new MemberReference( + // new TypeReference(module, module.CorLibTypeFactory.CorLibScope, "System.Runtime.CompilerServices"u8, "ExtensionAttribute"u8), + // ".ctor"u8, + // MethodSignature.CreateInstance(module.CorLibTypeFactory.Void) + // ) + // )); + // + // method.IsStatic = true; + // + // method.Signature.HasThis = false; + // method.Signature.ParameterTypes.Insert(0, GetParameterType(method.DeclaringType)); + // + // foreach (var definition in method.ParameterDefinitions) + // { + // definition.Sequence++; + // } + // + // method.Parameters.PullUpdatesFromMethodSignature(); + // method.Parameters[0].GetOrCreateDefinition().Name = "this"; + // } + + // private static TypeSignature GetParameterType(TypeDefinition type) + // { + // TypeSignature result; + // if (type.GenericParameters.Count > 0) + // { + // var genArgs = new TypeSignature[type.GenericParameters.Count]; + // for (var i = 0; i < genArgs.Length; i++) + // genArgs[i] = new GenericParameterSignature(type.Module, GenericParameterType.Type, i); + // result = type.MakeGenericInstanceType(genArgs); + // } + // else + // { + // result = type.ToTypeSignature(); + // } + // + // if (type.IsValueType) + // result = result.MakeByReferenceType(); + // + // return result; + // } +} diff --git a/CompatUnbreaker.Tool/Utilities/AsmResolver/DefinitionModifiersExtensions.cs b/CompatUnbreaker.Tool/Utilities/AsmResolver/DefinitionModifiersExtensions.cs new file mode 100644 index 0000000..ccfd58c --- /dev/null +++ b/CompatUnbreaker.Tool/Utilities/AsmResolver/DefinitionModifiersExtensions.cs @@ -0,0 +1,49 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.RegularExpressions; +using AsmResolver.DotNet; + +namespace CompatUnbreaker.Tool.Utilities.AsmResolver; + +internal static partial class DefinitionModifiersExtensions +{ + public static bool HasIsReadOnlyAttribute(this IHasCustomAttribute hasCustomAttribute) + { + return hasCustomAttribute.HasCustomAttribute("System.Runtime.CompilerServices", "IsReadOnlyAttribute"); + } + + public static bool IsReadOnly(this MethodDefinition method) + { + return method.HasIsReadOnlyAttribute(); + } + + public static bool IsReadOnly(this PropertyDefinition property) + { + property = property.GetLeastOverriddenMember(property.DeclaringType); + return property.SetMethod == null; + } + + [SuppressMessage("Design", "MA0138:Do not use \'Async\' suffix when a method does not return an awaitable type", Justification = "It's not actually async")] + public static bool IsAsync(this MethodDefinition method) + { + return method.HasCustomAttribute("System.Runtime.CompilerServices", "AsyncStateMachineAttribute"); + } + + public static bool IsVolatile(this FieldDefinition field) + { + return field.Signature?.FieldType.HasRequiredCustomModifier("System.Runtime.CompilerServices", "IsVolatile") == true; + } + + public static bool IsRequired(this IMemberDefinition member) + { + return member is IHasCustomAttribute hasCustomAttribute && + hasCustomAttribute.HasCustomAttribute("System.Runtime.CompilerServices", "RequiredMemberAttribute"); + } + + [GeneratedRegex("<([a-zA-Z_0-9]*)>F([0-9A-F]{64})__", RegexOptions.Compiled | RegexOptions.ExplicitCapture, matchTimeoutMilliseconds: 100)] + private static partial Regex FileTypeOrdinalPattern { get; } + + public static bool IsFileLocal(this TypeDefinition type) + { + return type.Name is not null && FileTypeOrdinalPattern.IsMatch(type.Name); + } +} diff --git a/CompatUnbreaker.Tool/Utilities/AsmResolver/ExtendedSignatureComparer.cs b/CompatUnbreaker.Tool/Utilities/AsmResolver/ExtendedSignatureComparer.cs new file mode 100644 index 0000000..cb15ebe --- /dev/null +++ b/CompatUnbreaker.Tool/Utilities/AsmResolver/ExtendedSignatureComparer.cs @@ -0,0 +1,91 @@ +using System.Diagnostics.CodeAnalysis; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; + +namespace CompatUnbreaker.Tool.Utilities.AsmResolver; + +// TODO upstream? + +/// +/// A with added support for , and . +/// +[SuppressMessage("Design", "CA1061:Do not hide base class methods", Justification = "Hidden base class methods eventually get called regardless.")] +internal sealed class ExtendedSignatureComparer : SignatureComparer, + IEqualityComparer, + IEqualityComparer, + IEqualityComparer +{ + public ExtendedSignatureComparer() + { + } + + public ExtendedSignatureComparer(SignatureComparisonFlags flags) : base(flags) + { + } + + public static new ExtendedSignatureComparer Default { get; } = new(); + public static ExtendedSignatureComparer VersionAgnostic { get; } = new(SignatureComparisonFlags.VersionAgnostic); + + public bool Equals(IMemberDescriptor? x, IMemberDescriptor? y) + { + if (ReferenceEquals(x, y)) return true; + if (x == null || y == null) return false; + + return x switch + { + ITypeDescriptor type => base.Equals(type, y as ITypeDescriptor), + IMethodDescriptor method => base.Equals(method, y as IMethodDescriptor), + IFieldDescriptor field => base.Equals(field, y as IFieldDescriptor), + PropertyDefinition property => Equals(property, y as PropertyDefinition), + EventDefinition @event => Equals(@event, y as EventDefinition), + _ => false, + }; + } + + public int GetHashCode(IMemberDescriptor obj) + { + return obj switch + { + ITypeDescriptor type => base.GetHashCode(type), + IMethodDescriptor method => base.GetHashCode(method), + IFieldDescriptor field => base.GetHashCode(field), + PropertyDefinition property => GetHashCode(property), + EventDefinition @event => GetHashCode(@event), + _ => throw new ArgumentOutOfRangeException(nameof(obj)), + }; + } + + public bool Equals(PropertyDefinition? x, PropertyDefinition? y) + { + if (ReferenceEquals(x, y)) return true; + if (x == null || y == null) return false; + + return x.Name == y.Name && base.Equals(x.DeclaringType, y.DeclaringType); + } + + public int GetHashCode(PropertyDefinition obj) + { + return HashCode.Combine( + obj.Name, + obj.DeclaringType == null ? 0 : base.GetHashCode(obj.DeclaringType), + obj.Signature == null ? 0 : base.GetHashCode(obj.Signature) + ); + } + + public bool Equals(EventDefinition? x, EventDefinition? y) + { + if (ReferenceEquals(x, y)) return true; + if (x == null || y == null) return false; + + return x.Name == y.Name && base.Equals(x.DeclaringType, y.DeclaringType); + } + + public int GetHashCode(EventDefinition obj) + { + return HashCode.Combine( + obj.Name, + obj.DeclaringType == null ? 0 : base.GetHashCode(obj.DeclaringType), + obj.EventType == null ? 0 : base.GetHashCode(obj.EventType) + ); + } +} diff --git a/CompatUnbreaker.Tool/Utilities/AsmResolver/GenericParameterExtensions.cs b/CompatUnbreaker.Tool/Utilities/AsmResolver/GenericParameterExtensions.cs new file mode 100644 index 0000000..4155d3b --- /dev/null +++ b/CompatUnbreaker.Tool/Utilities/AsmResolver/GenericParameterExtensions.cs @@ -0,0 +1,13 @@ +using AsmResolver.DotNet; + +namespace CompatUnbreaker.Tool.Utilities.AsmResolver; + +internal static class GenericParameterExtensions +{ + public static bool HasUnmanagedTypeConstraint(this GenericParameter genericParameter) + { + return genericParameter.HasNotNullableValueTypeConstraint && + genericParameter.HasCustomAttribute("System.Runtime.CompilerServices", "IsUnmanagedAttribute") && + genericParameter.Constraints.Any(c => c.Constraint?.ToTypeSignature().HasRequiredCustomModifier("System.Runtime.InteropServices", "UnmanagedType") == true); + } +} diff --git a/CompatUnbreaker.Tool/Utilities/AsmResolver/MemberDefinitionExtensions.cs b/CompatUnbreaker.Tool/Utilities/AsmResolver/MemberDefinitionExtensions.cs new file mode 100644 index 0000000..24655b2 --- /dev/null +++ b/CompatUnbreaker.Tool/Utilities/AsmResolver/MemberDefinitionExtensions.cs @@ -0,0 +1,88 @@ +using AsmResolver.DotNet; + +namespace CompatUnbreaker.Tool.Utilities.AsmResolver; + +internal static class MemberDefinitionExtensions +{ + public static bool IsRoslynAbstract(this IMemberDefinition member) + { + return member switch + { + TypeDefinition type => type is { IsAbstract: true, IsSealed: false }, + MethodDefinition method => method.IsAbstract, + FieldDefinition => false, + PropertyDefinition property => property.GetMethod is { IsAbstract: true } || property.SetMethod is { IsAbstract: true }, + EventDefinition @event => @event.AddMethod is { IsAbstract: true } || @event.RemoveMethod is { IsAbstract: true }, + _ => throw new ArgumentOutOfRangeException(nameof(member)), + }; + } + + public static bool IsRoslynSealed(this IMemberDefinition member) + { + return member switch + { + TypeDefinition type => type is { IsSealed: true, IsAbstract: false }, + MethodDefinition method => method.IsFinal && + (method.DeclaringType is { IsInterface: true } + ? method is { IsAbstract: true, IsVirtual: true, IsNewSlot: false } + : !method.IsAbstract && method.IsOverride()), + FieldDefinition => false, + PropertyDefinition property => property.GetMethod?.IsRoslynSealed() != false && property.SetMethod?.IsRoslynSealed() != false, + EventDefinition @event => @event.AddMethod?.IsRoslynSealed() == true || @event.RemoveMethod?.IsRoslynSealed() == true, + _ => throw new ArgumentOutOfRangeException(nameof(member)), + }; + } + + public static bool IsOverride(this IMemberDefinition member) + { + return member switch + { + TypeDefinition => false, + MethodDefinition method => method.DeclaringType is not { IsInterface: true } && + method.IsVirtual && + !method.IsDestructor() && + ((!method.IsNewSlot && method.DeclaringType?.BaseType != null) || method.IsExplicitClassOverride()), + FieldDefinition => false, + PropertyDefinition property => property.GetMethod?.IsOverride() == true || property.SetMethod?.IsOverride() == true, + EventDefinition @event => @event.AddMethod?.IsOverride() == true || @event.RemoveMethod?.IsOverride() == true, + _ => throw new ArgumentOutOfRangeException(nameof(member)), + }; + } + + public static bool IsRoslynVirtual(this IMemberDefinition member) + { + return member switch + { + TypeDefinition => false, + MethodDefinition method => method.IsVirtual && !method.IsDestructor() && !method.IsFinal && !method.IsRoslynAbstract() && + (method.DeclaringType?.IsInterface == true + ? method.IsStatic || method.IsNewSlot + : !method.IsOverride()), + FieldDefinition => false, + PropertyDefinition property => !property.IsOverride() && !property.IsRoslynAbstract() && + (property.GetMethod?.IsRoslynVirtual() == true || property.SetMethod?.IsRoslynVirtual() == true), + EventDefinition @event => !@event.IsOverride() && !@event.IsRoslynAbstract() && + (@event.AddMethod?.IsRoslynVirtual() == true || @event.RemoveMethod?.IsRoslynVirtual() == true), + _ => throw new ArgumentOutOfRangeException(nameof(member)), + }; + } + + public static bool IsExtern(this IMemberDefinition member) + { + return member switch + { + TypeDefinition => false, + MethodDefinition method => method.IsPInvokeImpl || method is { IsAbstract: false, HasMethodBody: false }, + FieldDefinition => false, + PropertyDefinition property => property.GetMethod?.IsExtern() == true || property.SetMethod?.IsExtern() == true, + EventDefinition @event => @event.AddMethod?.IsExtern() == true || @event.RemoveMethod?.IsExtern() == true, + _ => throw new ArgumentOutOfRangeException(nameof(member)), + }; + } + + public static T GetLeastOverriddenMember(this T member, TypeDefinition? accessingTypeOpt) + where T : IMemberDefinition + { + return member; // TODO + } +} diff --git a/CompatUnbreaker.Tool/Utilities/AsmResolver/MethodDefinitionExtensions.cs b/CompatUnbreaker.Tool/Utilities/AsmResolver/MethodDefinitionExtensions.cs new file mode 100644 index 0000000..080958d --- /dev/null +++ b/CompatUnbreaker.Tool/Utilities/AsmResolver/MethodDefinitionExtensions.cs @@ -0,0 +1,111 @@ +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using AsmResolver.PE.DotNet.Metadata.Tables; + +namespace CompatUnbreaker.Tool.Utilities.AsmResolver; + +internal static class MethodDefinitionExtensions +{ + public static bool IsDestructor(this MethodDefinition method) + { + if (method.DeclaringType == null || method.DeclaringType.IsInterface || + method.IsStatic || method.Name != "Finalize") + { + return false; + } + + foreach (var methodImplementation in method.DeclaringType.MethodImplementations) + { + if (methodImplementation.Body == method) + { + var declaration = methodImplementation.Declaration; + if (declaration != null && + declaration.DeclaringType?.ToTypeSignature() is CorLibTypeSignature { ElementType: ElementType.Object } && + declaration.Name == "Finalize") + { + return true; + } + } + } + + return false; + } + + public static bool IsExplicitInterfaceImplementation(this MethodDefinition method) + { + if (method is { IsVirtual: true, IsFinal: true, DeclaringType: not null }) + { + foreach (var implementation in method.DeclaringType.MethodImplementations) + { + if (implementation.Body == method) + { + return true; + } + } + } + + return false; + } + + public static bool IsExplicitClassOverride(this MethodDefinition method) + { + if (method.DeclaringType == null) + { + return false; + } + + foreach (var methodImplementation in method.DeclaringType.MethodImplementations) + { + if (methodImplementation.Body == method) + { + if (methodImplementation.Declaration?.DeclaringType?.Resolve()?.IsInterface == false) + { + return true; + } + } + } + + return false; + } + + public static bool IsPropertyAccessor(this MethodDefinition method) + { + if (method.DeclaringType == null) return false; + + foreach (var property in method.DeclaringType.Properties) + { + if (property.GetMethod == method || property.SetMethod == method) + { + return true; + } + } + + return false; + } + + public static bool IsEventAccessor(this MethodDefinition method) + { + if (method.DeclaringType == null) return false; + + foreach (var @event in method.DeclaringType.Events) + { + if (@event.AddMethod == method || @event.RemoveMethod == method) + { + return true; + } + } + + return false; + } + + public static bool IsParams(this MethodDefinition method) + { + return method.Parameters.Count != 0 && method.Parameters[^1].Definition?.IsParams() == true; + } + + public static bool IsParams(this ParameterDefinition parameter) + { + return parameter.HasCustomAttribute("System", "ParamArrayAttribute") || + parameter.HasCustomAttribute("System.Runtime.CompilerServices", "ParamCollectionAttribute"); + } +} diff --git a/CompatUnbreaker.Tool/Utilities/AsmResolver/MiscellaneousExtensions.cs b/CompatUnbreaker.Tool/Utilities/AsmResolver/MiscellaneousExtensions.cs new file mode 100644 index 0000000..60d8de2 --- /dev/null +++ b/CompatUnbreaker.Tool/Utilities/AsmResolver/MiscellaneousExtensions.cs @@ -0,0 +1,33 @@ +using AsmResolver.PE.DotNet.Cil; + +namespace CompatUnbreaker.Tool.Utilities.AsmResolver; + +internal static class MiscellaneousExtensions +{ + public static bool Match(this IEnumerable instructions, params ReadOnlySpan> predicates) + { + using var enumerator = instructions.GetEnumerator(); + + foreach (var predicate in predicates) + { + CilInstruction instruction; + + do + { + if (!enumerator.MoveNext()) + { + return false; + } + + instruction = enumerator.Current; + } while (instruction.OpCode == CilOpCodes.Nop); + + if (!predicate(instruction)) + { + return false; + } + } + + return !enumerator.MoveNext(); + } +} diff --git a/CompatUnbreaker.Tool/Utilities/AsmResolver/TypeDefinitionExtensions.InterfaceMapping.cs b/CompatUnbreaker.Tool/Utilities/AsmResolver/TypeDefinitionExtensions.InterfaceMapping.cs new file mode 100644 index 0000000..ec05e14 --- /dev/null +++ b/CompatUnbreaker.Tool/Utilities/AsmResolver/TypeDefinitionExtensions.InterfaceMapping.cs @@ -0,0 +1,42 @@ +using AsmResolver.DotNet; + +namespace CompatUnbreaker.Tool.Utilities.AsmResolver; + +internal static partial class TypeDefinitionExtensions +{ + public static IMemberDefinition? FindImplementationForInterfaceMember(this TypeDefinition type, IMemberDefinition interfaceMember) + { + if (!interfaceMember.IsImplementableInterfaceMember()) + { + return null; + } + + if (type.IsInterface) + { + // TODO + } + + return type.FindImplementationForInterfaceMemberInNonInterface(interfaceMember); + } + + private static IMemberDefinition? FindImplementationForInterfaceMemberInNonInterface(this TypeDefinition type, IMemberDefinition interfaceMember) + { + var interfaceType = interfaceMember.DeclaringType; + if (interfaceType == null || !interfaceType.IsInterface) + { + return null; + } + + if (interfaceMember is not (MethodDefinition or PropertyDefinition or EventDefinition)) + { + return null; + } + + return null; // TODO + } + + private static bool IsImplementableInterfaceMember(this IMemberDefinition member) + { + return !member.IsRoslynSealed() && (member.IsRoslynAbstract() || member.IsRoslynVirtual()) && (member.DeclaringType?.IsInterface ?? false); + } +} diff --git a/CompatUnbreaker.Tool/Utilities/AsmResolver/TypeDefinitionExtensions.cs b/CompatUnbreaker.Tool/Utilities/AsmResolver/TypeDefinitionExtensions.cs new file mode 100644 index 0000000..19eefd1 --- /dev/null +++ b/CompatUnbreaker.Tool/Utilities/AsmResolver/TypeDefinitionExtensions.cs @@ -0,0 +1,87 @@ +using AsmResolver.DotNet; + +namespace CompatUnbreaker.Tool.Utilities.AsmResolver; + +internal static partial class TypeDefinitionExtensions +{ + public static ReadOnlySpan GetUnmangledName(this TypeDefinition typeDefinition) + { + var arity = typeDefinition.GenericParameters.Count; + if (arity == 0) return typeDefinition.Name; + return MetadataHelpers.UnmangleMetadataNameForArity(typeDefinition.Name, arity); + } + + public static bool IsRecord(this TypeDefinition type) + { + return type.Methods.Any(m => m.Name == "$"); + } + + public static IEnumerable GetAllBaseTypes(this TypeDefinition type) + { + if (type.IsInterface) + { + foreach (var interfaceImplementation in type.Interfaces) + { + if (interfaceImplementation.Interface == null) continue; + + var @interface = interfaceImplementation.Interface.Resolve() + ?? throw new InvalidOperationException($"Couldn't resolve interface '{interfaceImplementation.Interface}'"); + + yield return @interface; + foreach (var baseInterface in @interface.GetAllBaseTypes()) + yield return baseInterface; + } + } + else if (type.BaseType != null) + { + var baseType = type.BaseType.Resolve() + ?? throw new InvalidOperationException($"Couldn't resolve base type '{type.BaseType}'"); + + yield return baseType; + foreach (var parentBaseType in baseType.GetAllBaseTypes()) + yield return parentBaseType; + } + } + + public static IEnumerable GetAllBaseInterfaces(this TypeDefinition type) + { + foreach (var interfaceImplementation in type.Interfaces) + { + if (interfaceImplementation.Interface == null) continue; + + var @interface = interfaceImplementation.Interface.Resolve() + ?? throw new InvalidOperationException($"Couldn't resolve interface '{interfaceImplementation.Interface}'"); + + yield return @interface; + foreach (var baseInterface in @interface.GetAllBaseInterfaces()) + yield return baseInterface; + } + + foreach (var baseType in type.GetAllBaseTypes()) + { + foreach (var baseInterface in baseType.GetAllBaseInterfaces()) + yield return baseInterface; + } + } + + public static IEnumerable GetMembers(this TypeDefinition type, bool includeNestedTypes = true) + { + foreach (var field in type.Fields) + yield return field; + + foreach (var property in type.Properties) + yield return property; + + foreach (var @event in type.Events) + yield return @event; + + foreach (var method in type.Methods) + yield return method; + + if (includeNestedTypes) + { + foreach (var nestedType in type.NestedTypes) + yield return nestedType; + } + } +} diff --git a/CompatUnbreaker.Tool/Utilities/AsmResolver/TypeSignatureExtensions.cs b/CompatUnbreaker.Tool/Utilities/AsmResolver/TypeSignatureExtensions.cs new file mode 100644 index 0000000..e3ec4e7 --- /dev/null +++ b/CompatUnbreaker.Tool/Utilities/AsmResolver/TypeSignatureExtensions.cs @@ -0,0 +1,32 @@ +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; + +namespace CompatUnbreaker.Tool.Utilities.AsmResolver; + +internal static class TypeSignatureExtensions +{ + public static bool HasCustomModifier(this TypeSignature signature, Func predicate) + { + while (signature is CustomModifierTypeSignature customModifierTypeSignature) + { + if (predicate(customModifierTypeSignature)) + { + return true; + } + + signature = customModifierTypeSignature.BaseType; + } + + return false; + } + + public static bool HasRequiredCustomModifier(this TypeSignature signature, string? @namespace, string? name) + { + return signature.HasCustomModifier(m => m.IsRequired && m.ModifierType.IsTypeOf(@namespace, name)); + } + + public static ReadOnlySpan GetUnmangledName(this GenericInstanceTypeSignature genericInstanceTypeSignature) + { + return MetadataHelpers.UnmangleMetadataNameForArity(genericInstanceTypeSignature.GenericType.Name, genericInstanceTypeSignature.TypeArguments.Count); + } +} diff --git a/CompatUnbreaker.Tool/Utilities/MetadataHelpers.cs b/CompatUnbreaker.Tool/Utilities/MetadataHelpers.cs new file mode 100644 index 0000000..87be013 --- /dev/null +++ b/CompatUnbreaker.Tool/Utilities/MetadataHelpers.cs @@ -0,0 +1,96 @@ +using System.Diagnostics; + +namespace CompatUnbreaker.Tool.Utilities; + +// Copied from https://github.com/dotnet/roslyn/blob/7c625024a1984d9f04f317940d518402f5898758/src/Compilers/Core/Portable/MetadataReader/MetadataHelpers.cs + +internal static class MetadataHelpers +{ + private const char GenericTypeNameManglingChar = '`'; + private const int MaxStringLengthForParamSize = 22; + + private static short InferTypeArityFromMetadataName(ReadOnlySpan emittedTypeName, out int suffixStartsAt) + { + Debug.Assert(!emittedTypeName.IsEmpty, "NULL actual name unexpected!!!"); + var emittedTypeNameLength = emittedTypeName.Length; + + int indexOfManglingChar; + for (indexOfManglingChar = emittedTypeNameLength; indexOfManglingChar >= 1; indexOfManglingChar--) + { + if (emittedTypeName[indexOfManglingChar - 1] == GenericTypeNameManglingChar) + { + break; + } + } + + if (indexOfManglingChar < 2 || + emittedTypeNameLength - indexOfManglingChar == 0 || + emittedTypeNameLength - indexOfManglingChar > MaxStringLengthForParamSize) + { + suffixStartsAt = -1; + return 0; + } + + // Given a name corresponding to `, extract the arity. + if (TryScanArity(emittedTypeName[indexOfManglingChar..]) is not { } arity) + { + suffixStartsAt = -1; + return 0; + } + + suffixStartsAt = indexOfManglingChar - 1; + return arity; + + static short? TryScanArity(ReadOnlySpan aritySpan) + { + // Arity must have at least one character and must not have leading zeroes. + // Also, in order to fit into short.MaxValue (32767), it must be at most 5 characters long. + if (aritySpan is { Length: >= 1 and <= 5 } and not ['0', ..]) + { + var intArity = 0; + foreach (var digit in aritySpan) + { + // Accepting integral decimal digits only + if (digit is < '0' or > '9') + return null; + + intArity = (intArity * 10) + (digit - '0'); + } + + Debug.Assert(intArity > 0); + + if (intArity <= short.MaxValue) + return (short) intArity; + } + + return null; + } + } + + public static ReadOnlySpan InferTypeArityAndUnmangleMetadataName(ReadOnlySpan emittedTypeName, out short arity) + { + arity = InferTypeArityFromMetadataName(emittedTypeName, out var suffixStartsAt); + + if (arity == 0) + { + Debug.Assert(suffixStartsAt == -1); + return emittedTypeName; + } + + Debug.Assert(suffixStartsAt > 0 && suffixStartsAt < emittedTypeName.Length - 1); + return emittedTypeName[..suffixStartsAt]; + } + + public static ReadOnlySpan UnmangleMetadataNameForArity(ReadOnlySpan emittedTypeName, int arity) + { + Debug.Assert(arity > 0); + + if (arity == InferTypeArityFromMetadataName(emittedTypeName, out var suffixStartsAt)) + { + Debug.Assert(suffixStartsAt > 0 && suffixStartsAt < emittedTypeName.Length - 1); + return emittedTypeName[..suffixStartsAt]; + } + + return emittedTypeName; + } +} diff --git a/CompatUnbreaker.Tool/Utilities/Roslyn/TypeSymbolExtensions.cs b/CompatUnbreaker.Tool/Utilities/Roslyn/TypeSymbolExtensions.cs new file mode 100644 index 0000000..ac0569a --- /dev/null +++ b/CompatUnbreaker.Tool/Utilities/Roslyn/TypeSymbolExtensions.cs @@ -0,0 +1,62 @@ +using System.Text; +using Microsoft.CodeAnalysis; + +namespace CompatUnbreaker.Tool.Utilities.Roslyn; + +internal static class TypeSymbolExtensions +{ + public static IEnumerable GetAllTypes(this INamespaceOrTypeSymbol symbol) + { + var queue = new Queue(); + queue.Enqueue(symbol); + + while (queue.Count > 0) + { + var member = queue.Dequeue(); + + if (member is INamespaceSymbol namespaceSymbol) + { + foreach (var namespaceMember in namespaceSymbol.GetMembers()) + { + queue.Enqueue(namespaceMember); + } + } + else if (member is ITypeSymbol namedTypeSymbol) + { + yield return namedTypeSymbol; + + foreach (var typeMember in namedTypeSymbol.GetTypeMembers()) + { + queue.Enqueue(typeMember); + } + } + else + { + throw new InvalidOperationException(); + } + } + } + + public static string GetFullMetadataName(this ITypeSymbol s) + { + var stringBuilder = new StringBuilder(s.MetadataName); + var current = s.ContainingSymbol; + + while (current is not INamespaceSymbol { IsGlobalNamespace: true }) + { + if (current is ITypeSymbol) + { + stringBuilder.Insert(0, "+"); + } + else if (current is INamespaceSymbol) + { + stringBuilder.Insert(0, "."); + } + + stringBuilder.Insert(0, current.MetadataName); + current = current.ContainingSymbol; + } + + return stringBuilder.ToString(); + } +} diff --git a/CompatUnbreaker.Tool/Utilities/StringExtensions.cs b/CompatUnbreaker.Tool/Utilities/StringExtensions.cs new file mode 100644 index 0000000..e588f73 --- /dev/null +++ b/CompatUnbreaker.Tool/Utilities/StringExtensions.cs @@ -0,0 +1,14 @@ +namespace CompatUnbreaker.Tool.Utilities; + +internal static class StringExtensions +{ + public static string TrimPrefix(this string text, string? prefix) + { + if (!string.IsNullOrEmpty(prefix) && text.StartsWith(prefix, StringComparison.Ordinal)) + { + return text[prefix.Length..]; + } + + return text; + } +} diff --git a/CompatUnbreaker.Tool/packages.lock.json b/CompatUnbreaker.Tool/packages.lock.json new file mode 100644 index 0000000..675eedd --- /dev/null +++ b/CompatUnbreaker.Tool/packages.lock.json @@ -0,0 +1,506 @@ +{ + "version": 1, + "dependencies": { + "net9.0": { + "Basic.Reference.Assemblies.Net90": { + "type": "Direct", + "requested": "[1.8.3, )", + "resolved": "1.8.3", + "contentHash": "sZRH0NW/jdLTNq8X2/IVGIUVVE0JLKxwSp9GOwKzoHzt7BK5U7iRqG25ddSsggdABpvyz3ooQ4GkwQQ/Kn+z+g==", + "dependencies": { + "Microsoft.CodeAnalysis.Common": "4.11.0" + } + }, + "ConsoleAppFramework": { + "type": "Direct", + "requested": "[5.6.2, )", + "resolved": "5.6.2", + "contentHash": "IAc33TLfXAM6bVQFIsvr8I7+bC3TGU9xiyuG8aRtCzIPFctvB28bchZP8BhI/7cuSNlhQDDRZb0YsmrhL4Lfcg==" + }, + "Meziantou.Analyzer": { + "type": "Direct", + "requested": "[2.0.220, )", + "resolved": "2.0.220", + "contentHash": "LknH5tduuxY6jzPGUIBHrszk1fepyEWAD9Ht7Hq1rrGZ02BKx54ADXzrlXTkLNpnkVzRBHU4bO6HH7GwIeDR+w==" + }, + "Microsoft.Build": { + "type": "Direct", + "requested": "[17.14.28, )", + "resolved": "17.14.28", + "contentHash": "MmGLEsROW1C9dH/d4sOqUX0sVNs2uwTCFXRQb89+pYNWDNJE+7bTJG9kOCbHeCH252XLnP55KIaOgwSpf6J4Kw==", + "dependencies": { + "Microsoft.Build.Framework": "17.14.28", + "Microsoft.NET.StringTools": "17.14.28", + "System.Configuration.ConfigurationManager": "9.0.0", + "System.Diagnostics.EventLog": "9.0.0", + "System.Reflection.MetadataLoadContext": "9.0.0", + "System.Security.Cryptography.ProtectedData": "9.0.0" + } + }, + "Microsoft.Build.Framework": { + "type": "Direct", + "requested": "[17.14.28, )", + "resolved": "17.14.28", + "contentHash": "wRcyTzGV0LRAtFdrddtioh59Ky4/zbvyraP0cQkDzRSRkhgAQb0K88D/JNC6VHLIXanRi3mtV1jU0uQkBwmiVg==" + }, + "Microsoft.Build.Locator": { + "type": "Direct", + "requested": "[1.10.2, )", + "resolved": "1.10.2", + "contentHash": "F+nLS7IpgtslyxNvtD6Jalnf5WU08lu8yfJBNQl3cbEF3AMUphs4t7nPuRYaaU8QZyGrqtVi7i73LhAe/yHx7A==" + }, + "Microsoft.Build.Tasks.Core": { + "type": "Direct", + "requested": "[17.14.28, )", + "resolved": "17.14.28", + "contentHash": "jk3O0tXp9QWPXhLJ7Pl8wm/eGtGgA1++vwHGWEmnwMU6eP//ghtcCUpQh9CQMwEKGDnH0aJf285V1s8yiSlKfQ==", + "dependencies": { + "Microsoft.Build.Framework": "17.14.28", + "Microsoft.Build.Utilities.Core": "17.14.28", + "Microsoft.NET.StringTools": "17.14.28", + "System.CodeDom": "9.0.0", + "System.Collections.Immutable": "9.0.0", + "System.Configuration.ConfigurationManager": "9.0.0", + "System.Diagnostics.EventLog": "9.0.0", + "System.Formats.Nrbf": "9.0.0", + "System.Resources.Extensions": "9.0.0", + "System.Security.Cryptography.Pkcs": "9.0.0", + "System.Security.Cryptography.ProtectedData": "9.0.0", + "System.Security.Cryptography.Xml": "9.0.0" + } + }, + "Microsoft.Build.Utilities.Core": { + "type": "Direct", + "requested": "[17.14.28, )", + "resolved": "17.14.28", + "contentHash": "rhSdPo8QfLXXWM+rY0x0z1G4KK4ZhMoIbHROyDj8MUBFab9nvHR0NaMnjzOgXldhmD2zi2ir8d6xCatNzlhF5g==", + "dependencies": { + "Microsoft.Build.Framework": "17.14.28", + "Microsoft.NET.StringTools": "17.14.28", + "System.Collections.Immutable": "9.0.0", + "System.Configuration.ConfigurationManager": "9.0.0", + "System.Diagnostics.EventLog": "9.0.0", + "System.Security.Cryptography.ProtectedData": "9.0.0" + } + }, + "Microsoft.CodeAnalysis.CSharp": { + "type": "Direct", + "requested": "[4.14.0, )", + "resolved": "4.14.0", + "contentHash": "568a6wcTivauIhbeWcCwfWwIn7UV7MeHEBvFB2uzGIpM2OhJ4eM/FZ8KS0yhPoNxnSpjGzz7x7CIjTxhslojQA==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Common": "[4.14.0]", + "System.Collections.Immutable": "9.0.0", + "System.Reflection.Metadata": "9.0.0" + } + }, + "Microsoft.CodeAnalysis.CSharp.Workspaces": { + "type": "Direct", + "requested": "[4.14.0, )", + "resolved": "4.14.0", + "contentHash": "QkgCEM4qJo6gdtblXtNgHqtykS61fxW+820hx5JN6n9DD4mQtqNB+6fPeJ3GQWg6jkkGz6oG9yZq7H3Gf0zwYw==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.CSharp": "[4.14.0]", + "Microsoft.CodeAnalysis.Common": "[4.14.0]", + "Microsoft.CodeAnalysis.Workspaces.Common": "[4.14.0]", + "System.Collections.Immutable": "9.0.0", + "System.Composition": "9.0.0", + "System.IO.Pipelines": "9.0.0", + "System.Reflection.Metadata": "9.0.0", + "System.Threading.Channels": "7.0.0" + } + }, + "Microsoft.CodeAnalysis.Workspaces.MSBuild": { + "type": "Direct", + "requested": "[4.14.0, )", + "resolved": "4.14.0", + "contentHash": "YU7Sguzm1Cuhi2U6S0DRKcVpqAdBd2QmatpyE0KqYMJogJ9E27KHOWGUzAOjsyjAM7sNaUk+a8VPz24knDseFw==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.Build": "17.7.2", + "Microsoft.Build.Framework": "17.7.2", + "Microsoft.Build.Tasks.Core": "17.7.2", + "Microsoft.Build.Utilities.Core": "17.7.2", + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Workspaces.Common": "[4.14.0]", + "Microsoft.Extensions.DependencyInjection": "9.0.0", + "Microsoft.Extensions.Logging": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0", + "Microsoft.Extensions.Primitives": "9.0.0", + "Newtonsoft.Json": "13.0.3", + "System.CodeDom": "7.0.0", + "System.Collections.Immutable": "9.0.0", + "System.Composition": "9.0.0", + "System.Configuration.ConfigurationManager": "9.0.0", + "System.Diagnostics.EventLog": "9.0.0", + "System.IO.Pipelines": "9.0.0", + "System.Reflection.Metadata": "9.0.0", + "System.Resources.Extensions": "9.0.0", + "System.Security.Cryptography.Pkcs": "7.0.2", + "System.Security.Cryptography.ProtectedData": "9.0.0", + "System.Security.Cryptography.Xml": "7.0.1", + "System.Security.Permissions": "9.0.0", + "System.Text.Json": "9.0.0", + "System.Threading.Channels": "7.0.0", + "System.Threading.Tasks.Dataflow": "9.0.0", + "System.Windows.Extensions": "9.0.0" + } + }, + "MonoMod.RuntimeDetour": { + "type": "Direct", + "requested": "[25.3.1, )", + "resolved": "25.3.1", + "contentHash": "QQ9Ng3E6enCMbcFNDSUqWBD4+KdnjfxConI8QzMAH9p7i2vvzehqYT8K4Mghq2YeTIVN6Aih8PlqcQxQk2uypQ==", + "dependencies": { + "Mono.Cecil": "0.11.6", + "MonoMod.Backports": "1.1.2", + "MonoMod.Core": "1.3.1", + "MonoMod.ILHelpers": "1.1.0", + "MonoMod.Utils": "25.0.9" + } + }, + "PolySharp": { + "type": "Direct", + "requested": "[1.15.0, )", + "resolved": "1.15.0", + "contentHash": "FbU0El+EEjdpuIX4iDbeS7ki1uzpJPx8vbqOzEtqnl1GZeAGJfq+jCbxeJL2y0EPnUNk8dRnnqR2xnYXg9Tf+g==" + }, + "StyleCop.Analyzers": { + "type": "Direct", + "requested": "[1.2.0-beta.556, )", + "resolved": "1.2.0-beta.556", + "contentHash": "llRPgmA1fhC0I0QyFLEcjvtM2239QzKr/tcnbsjArLMJxJlu0AA5G7Fft0OI30pHF3MW63Gf4aSSsjc5m82J1Q==", + "dependencies": { + "StyleCop.Analyzers.Unstable": "1.2.0.556" + } + }, + "AsmResolver": { + "type": "Transitive", + "resolved": "6.0.0-dev" + }, + "AsmResolver.DotNet": { + "type": "Transitive", + "resolved": "6.0.0-dev", + "dependencies": { + "AsmResolver.PE": "6.0.0-dev" + } + }, + "AsmResolver.PE": { + "type": "Transitive", + "resolved": "6.0.0-dev", + "dependencies": { + "AsmResolver.PE.File": "6.0.0-dev" + } + }, + "AsmResolver.PE.File": { + "type": "Transitive", + "resolved": "6.0.0-dev", + "dependencies": { + "AsmResolver": "6.0.0-dev" + } + }, + "Humanizer.Core": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==" + }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Transitive", + "resolved": "3.11.0", + "contentHash": "v/EW3UE8/lbEYHoC2Qq7AR/DnmvpgdtAMndfQNmpuIMx/Mto8L5JnuCfdBYtgvalQOtfNCnxFejxuRrryvUTsg==" + }, + "Microsoft.CodeAnalysis.Common": { + "type": "Transitive", + "resolved": "4.14.0", + "contentHash": "PC3tuwZYnC+idaPuoC/AZpEdwrtX7qFpmnrfQkgobGIWiYmGi5MCRtl5mx6QrfMGQpK78X2lfIEoZDLg/qnuHg==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "System.Collections.Immutable": "9.0.0", + "System.Reflection.Metadata": "9.0.0" + } + }, + "Microsoft.CodeAnalysis.Workspaces.Common": { + "type": "Transitive", + "resolved": "4.14.0", + "contentHash": "wNVK9JrqjqDC/WgBUFV6henDfrW87NPfo98nzah/+M/G1D6sBOPtXwqce3UQNn+6AjTnmkHYN1WV9XmTlPemTw==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Common": "[4.14.0]", + "System.Collections.Immutable": "9.0.0", + "System.Composition": "9.0.0", + "System.IO.Pipelines": "9.0.0", + "System.Reflection.Metadata": "9.0.0", + "System.Threading.Channels": "7.0.0" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "MCPrg7v3QgNMr0vX4vzRXvkNGgLg8vKWX0nKCWUxu2uPyMsaRgiRc1tHBnbTcfJMhMKj2slE/j2M9oGkd25DNw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "+6f2qv2a3dLwd5w6JanPIPs47CxRbnk+ZocMJUhv9NxP88VlOcJYZs9jY+MYSjxvady08bUZn6qgiNh7DadGgg==" + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "crjWyORoug0kK7RSNJBTeSE6VX8IQgLf3nUpTB9m62bPXp/tzbnOsnbe8TXEG0AASNaKZddnpHKw7fET8E++Pg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "g0UfujELzlLbHoVG8kPKVBaW470Ewi+jnptGS9KUi6jcb+k2StujtK3m26DFSGGwQ/+bVgZfsWqNzlP6YOejvw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0" + } + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "y2146b3jrPI3Q0lokKXdKLpmXqakYbDIPDV6r3M8SqvSf45WwOTzkyfDpxnZXJsJQEpAsAqjUq5Pu8RCJMjubg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "Microsoft.Extensions.Primitives": "9.0.0" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "N3qEBzmLMYiASUlKxxFIISP4AiwuPTHF5uCh+2CWSwwzAJiIYx0kBJsS30cp1nvhSySFAVi30jecD307jV+8Kg==" + }, + "Microsoft.NET.StringTools": { + "type": "Transitive", + "resolved": "17.14.28", + "contentHash": "DMIeWDlxe0Wz0DIhJZ2FMoGQAN2yrGZOi5jjFhRYHWR5ONd0CS6IpAHlRnA7uA/5BF+BADvgsETxW2XrPiFc1A==" + }, + "Mono.Cecil": { + "type": "Transitive", + "resolved": "0.11.6", + "contentHash": "f33RkDtZO8VlGXCtmQIviOtxgnUdym9xx/b1p9h91CRGOsJFxCFOFK1FDbVt1OCf1aWwYejUFa2MOQyFWTFjbA==" + }, + "MonoMod.Backports": { + "type": "Transitive", + "resolved": "1.1.2", + "contentHash": "baYlNy8n8kmaNhNvqmZ/dIPOeO1r9//dG1i2WbunMWtWZ2EKtIgmXaS+ZzphzTsikkGnoD4Jwr5g0TVdpDjgpw==", + "dependencies": { + "MonoMod.ILHelpers": "1.1.0" + } + }, + "MonoMod.Core": { + "type": "Transitive", + "resolved": "1.3.1", + "contentHash": "AE78s2Iv74mFfkbH+iUAGmVpf+zkMlJYOtQPVVuRN4Eorcy7Dm4wps1X/CVp4p6a7MnbCKOuQz9OeVC/xJN6+Q==", + "dependencies": { + "Mono.Cecil": "0.11.6", + "MonoMod.Backports": "1.1.2", + "MonoMod.ILHelpers": "1.1.0", + "MonoMod.Utils": "25.0.9" + } + }, + "MonoMod.ILHelpers": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "L2FWjhTrv7tcIxshfZ+M3OcaNr4cNw0IwiVZEgwqRnZ5QAN3+RrNJ8ZwCzwXUWyPDqooJxMcjjg8PsSYUiNBjQ==" + }, + "MonoMod.Utils": { + "type": "Transitive", + "resolved": "25.0.9", + "contentHash": "KPZ/VAr4zwT12/OJPZF2uazc9M/tRjiizeErq4m5Ie4EA6XnreakLfs8RlvxszgwXL7FVGnkg9JU5tKtQRPGMA==", + "dependencies": { + "Mono.Cecil": "0.11.6", + "MonoMod.Backports": "1.1.2", + "MonoMod.ILHelpers": "1.1.0" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "StyleCop.Analyzers.Unstable": { + "type": "Transitive", + "resolved": "1.2.0.556", + "contentHash": "zvn9Mqs/ox/83cpYPignI8hJEM2A93s2HkHs8HYMOAQW0PkampyoErAiIyKxgTLqbbad29HX/shv/6LGSjPJNQ==" + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "oTE5IfuMoET8yaZP/vdvy9xO47guAv/rOhe4DODuFBN3ySprcQOlXqO3j+e/H/YpKKR5sglrxRaZ2HYOhNJrqA==" + }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "QhkXUl2gNrQtvPmtBTQHb0YsUrDiDQ2QS09YbtTTiSjGcf7NBqtYbrG/BE06zcBPCKEwQGzIv13IVdXNOSub2w==" + }, + "System.Composition": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "3Djj70fFTraOarSKmRnmRy/zm4YurICm+kiCtI0dYRqGJnLX6nJ+G3WYuFJ173cAPax/gh96REcbNiVqcrypFQ==", + "dependencies": { + "System.Composition.AttributedModel": "9.0.0", + "System.Composition.Convention": "9.0.0", + "System.Composition.Hosting": "9.0.0", + "System.Composition.Runtime": "9.0.0", + "System.Composition.TypedParts": "9.0.0" + } + }, + "System.Composition.AttributedModel": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "iri00l/zIX9g4lHMY+Nz0qV1n40+jFYAmgsaiNn16xvt2RDwlqByNG4wgblagnDYxm3YSQQ0jLlC/7Xlk9CzyA==" + }, + "System.Composition.Convention": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "+vuqVP6xpi582XIjJi6OCsIxuoTZfR0M7WWufk3uGDeCl3wGW6KnpylUJ3iiXdPByPE0vR5TjJgR6hDLez4FQg==", + "dependencies": { + "System.Composition.AttributedModel": "9.0.0" + } + }, + "System.Composition.Hosting": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "OFqSeFeJYr7kHxDfaViGM1ymk7d4JxK//VSoNF9Ux0gpqkLsauDZpu89kTHHNdCWfSljbFcvAafGyBoY094btQ==", + "dependencies": { + "System.Composition.Runtime": "9.0.0" + } + }, + "System.Composition.Runtime": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "w1HOlQY1zsOWYussjFGZCEYF2UZXgvoYnS94NIu2CBnAGMbXFAX8PY8c92KwUItPmowal68jnVLBCzdrWLeEKA==" + }, + "System.Composition.TypedParts": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "aRZlojCCGEHDKqh43jaDgaVpYETsgd7Nx4g1zwLKMtv4iTo0627715ajEFNpEEBTgLmvZuv8K0EVxc3sM4NWJA==", + "dependencies": { + "System.Composition.AttributedModel": "9.0.0", + "System.Composition.Hosting": "9.0.0", + "System.Composition.Runtime": "9.0.0" + } + }, + "System.Configuration.ConfigurationManager": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "PdkuMrwDhXoKFo/JxISIi9E8L+QGn9Iquj2OKDWHB6Y/HnUOuBouF7uS3R4Hw3FoNmwwMo6hWgazQdyHIIs27A==", + "dependencies": { + "System.Diagnostics.EventLog": "9.0.0", + "System.Security.Cryptography.ProtectedData": "9.0.0" + } + }, + "System.Diagnostics.EventLog": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "qd01+AqPhbAG14KtdtIqFk+cxHQFZ/oqRSCoxU1F+Q6Kv0cl726sl7RzU9yLFGd4BUOKdN4XojXF0pQf/R6YeA==" + }, + "System.Formats.Nrbf": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "F/6tNE+ckmdFeSQAyQo26bQOqfPFKEfZcuqnp4kBE6/7jP26diP+QTHCJJ6vpEfaY6bLy+hBLiIQUSxSmNwLkA==" + }, + "System.IO.Pipelines": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "eA3cinogwaNB4jdjQHOP3Z3EuyiDII7MT35jgtnsA4vkn0LUrrSHsU0nzHTzFzmaFYeKV7MYyMxOocFzsBHpTw==" + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "ANiqLu3DxW9kol/hMmTWbt3414t9ftdIuiIU7j80okq2YzAueo120M442xk1kDJWtmZTqWQn7wHDvMRipVOEOQ==" + }, + "System.Reflection.MetadataLoadContext": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "nGdCUVhEQ9/CWYqgaibYEDwIJjokgIinQhCnpmtZfSXdMS6ysLZ8p9xvcJ8VPx6Xpv5OsLIUrho4B9FN+VV/tw==" + }, + "System.Resources.Extensions": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "tvhuT1D2OwPROdL1kRWtaTJliQo0WdyhvwDpd8RM997G7m3Hya5nhbYhNTS75x6Vu+ypSOgL5qxDCn8IROtCxw==", + "dependencies": { + "System.Formats.Nrbf": "9.0.0" + } + }, + "System.Security.Cryptography.Pkcs": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "8tluJF8w9si+2yoHeL8rgVJS6lKvWomTDC8px65Z8MCzzdME5eaPtEQf4OfVGrAxB5fW93ncucy1+221O9EQaw==" + }, + "System.Security.Cryptography.ProtectedData": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "CJW+x/F6fmRQ7N6K8paasTw9PDZp4t7G76UjGNlSDgoHPF0h08vTzLYbLZpOLEJSg35d5wy2jCXGo84EN05DpQ==" + }, + "System.Security.Cryptography.Xml": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "GQZn5wFd+pyOfwWaCbqxG7trQ5ox01oR8kYgWflgtux4HiUNihGEgG2TktRWyH+9bw7NoEju1D41H/upwQeFQw==", + "dependencies": { + "System.Security.Cryptography.Pkcs": "9.0.0" + } + }, + "System.Security.Permissions": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "H2VFD4SFVxieywNxn9/epb63/IOcPPfA0WOtfkljzNfu7GCcHIBQNuwP6zGCEIi7Ci/oj8aLPUNK9sYImMFf4Q==", + "dependencies": { + "System.Windows.Extensions": "9.0.0" + } + }, + "System.Text.Json": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "js7+qAu/9mQvnhA4EfGMZNEzXtJCDxgkgj8ohuxq/Qxv+R56G+ljefhiJHOxTNiw54q8vmABCWUwkMulNdlZ4A==" + }, + "System.Threading.Channels": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "qmeeYNROMsONF6ndEZcIQ+VxR4Q/TX/7uIVLJqtwIWL7dDWeh0l1UIqgo4wYyjG//5lUNhwkLDSFl+pAWO6oiA==" + }, + "System.Threading.Tasks.Dataflow": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "S+y+QuBJNcqOvoFK+rFcZZuQDlD2E4lImKW9/g3E0l7YT2uo4oin9amAn398eGt/xFBYNNSt5O77Dbc38XGfBw==" + }, + "System.Windows.Extensions": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "U9msthvnH2Fsw7xwAvIhNHOdnIjOQTwOc8Vd0oGOsiRcGMGoBFlUD6qtYawRUoQdKH9ysxesZ9juFElt1Jw/7A==" + }, + "compatunbreaker": { + "type": "Project", + "dependencies": { + "AsmResolver.DotNet": "[6.0.0-dev, )", + "CompatUnbreaker.Attributes": "[1.0.0-dev, )", + "Meziantou.Analyzer": "[2.0.220, )", + "PolySharp": "[1.15.0, )", + "StyleCop.Analyzers": "[1.2.0-beta.556, )" + } + }, + "compatunbreaker.attributes": { + "type": "Project", + "dependencies": { + "Meziantou.Analyzer": "[2.0.220, )", + "PolySharp": "[1.15.0, )", + "StyleCop.Analyzers": "[1.2.0-beta.556, )" + } + } + } + } +} \ No newline at end of file diff --git a/CompatUnbreaker.sln b/CompatUnbreaker.sln new file mode 100644 index 0000000..88ced00 --- /dev/null +++ b/CompatUnbreaker.sln @@ -0,0 +1,52 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompatUnbreaker", "CompatUnbreaker\CompatUnbreaker.csproj", "{106A0BD3-DF0A-4DEE-81FD-3BE81A4B342D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompatUnbreaker.Tool", "CompatUnbreaker.Tool\CompatUnbreaker.Tool.csproj", "{E5EC6CFD-819E-46C0-9C87-9EE1E4B802BE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompatUnbreaker.Attributes", "CompatUnbreaker.Attributes\CompatUnbreaker.Attributes.csproj", "{42F48A69-3114-4ADE-9892-41ABA5435FCB}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Playground", "Playground", "{9B9BB47F-F286-400F-8535-5B493FC4D738}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlaygroundLibrary.V1", "PlaygroundLibrary\PlaygroundLibrary.V1.csproj", "{6EF8D01D-B0C6-4B06-BD7E-C450513E8B7A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlaygroundLibrary.V2", "PlaygroundLibrary\PlaygroundLibrary.V2.csproj", "{1B3331D2-62F5-4351-9715-5D2AC1DAF85F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompatUnbreaker.Tests", "CompatUnbreaker.Tests\CompatUnbreaker.Tests.csproj", "{9F2362A1-2505-4128-895B-97EC55E81758}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {106A0BD3-DF0A-4DEE-81FD-3BE81A4B342D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {106A0BD3-DF0A-4DEE-81FD-3BE81A4B342D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {106A0BD3-DF0A-4DEE-81FD-3BE81A4B342D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {106A0BD3-DF0A-4DEE-81FD-3BE81A4B342D}.Release|Any CPU.Build.0 = Release|Any CPU + {E5EC6CFD-819E-46C0-9C87-9EE1E4B802BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E5EC6CFD-819E-46C0-9C87-9EE1E4B802BE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E5EC6CFD-819E-46C0-9C87-9EE1E4B802BE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E5EC6CFD-819E-46C0-9C87-9EE1E4B802BE}.Release|Any CPU.Build.0 = Release|Any CPU + {42F48A69-3114-4ADE-9892-41ABA5435FCB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {42F48A69-3114-4ADE-9892-41ABA5435FCB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {42F48A69-3114-4ADE-9892-41ABA5435FCB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {42F48A69-3114-4ADE-9892-41ABA5435FCB}.Release|Any CPU.Build.0 = Release|Any CPU + {6EF8D01D-B0C6-4B06-BD7E-C450513E8B7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6EF8D01D-B0C6-4B06-BD7E-C450513E8B7A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6EF8D01D-B0C6-4B06-BD7E-C450513E8B7A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6EF8D01D-B0C6-4B06-BD7E-C450513E8B7A}.Release|Any CPU.Build.0 = Release|Any CPU + {1B3331D2-62F5-4351-9715-5D2AC1DAF85F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1B3331D2-62F5-4351-9715-5D2AC1DAF85F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1B3331D2-62F5-4351-9715-5D2AC1DAF85F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1B3331D2-62F5-4351-9715-5D2AC1DAF85F}.Release|Any CPU.Build.0 = Release|Any CPU + {9F2362A1-2505-4128-895B-97EC55E81758}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9F2362A1-2505-4128-895B-97EC55E81758}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9F2362A1-2505-4128-895B-97EC55E81758}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9F2362A1-2505-4128-895B-97EC55E81758}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {6EF8D01D-B0C6-4B06-BD7E-C450513E8B7A} = {9B9BB47F-F286-400F-8535-5B493FC4D738} + {1B3331D2-62F5-4351-9715-5D2AC1DAF85F} = {9B9BB47F-F286-400F-8535-5B493FC4D738} + EndGlobalSection +EndGlobal diff --git a/CompatUnbreaker/CompatUnbreaker.csproj b/CompatUnbreaker/CompatUnbreaker.csproj new file mode 100644 index 0000000..8e792fe --- /dev/null +++ b/CompatUnbreaker/CompatUnbreaker.csproj @@ -0,0 +1,25 @@ + + + true + + net9.0;net6.0 + + + + + + + + + + + + + + + + + + + + diff --git a/CompatUnbreaker/Models/Attributes/AttributeDescription.cs b/CompatUnbreaker/Models/Attributes/AttributeDescription.cs new file mode 100644 index 0000000..c283c95 --- /dev/null +++ b/CompatUnbreaker/Models/Attributes/AttributeDescription.cs @@ -0,0 +1,50 @@ +using AsmResolver; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using AsmResolver.PE.DotNet.Metadata.Tables; + +namespace CompatUnbreaker.Models.Attributes; + +internal static class AttributeDescription +{ + private static Utf8String CompatUnbreakerAttributesNamespace { get; } = "CompatUnbreaker.Attributes"u8; + + public static MarkerAttributeDescription UnbreakerConstructorAttribute { get; } = new(CompatUnbreakerAttributesNamespace, "UnbreakerConstructorAttribute"u8); + public static SingleValueAttributeDescription UnbreakerExtensionAttribute { get; } = new(CompatUnbreakerAttributesNamespace, "UnbreakerExtensionAttribute"u8); + public static MarkerAttributeDescription UnbreakerExtensionsAttribute { get; } = new(CompatUnbreakerAttributesNamespace, "UnbreakerExtensionsAttribute"u8); + + public static MarkerAttributeDescription UnbreakerFieldAttribute { get; } = new(CompatUnbreakerAttributesNamespace, "UnbreakerFieldAttribute"u8); + + public static UnbreakerRenameAttributeDescription UnbreakerRenameAttribute { get; } = new(CompatUnbreakerAttributesNamespace, "UnbreakerRenameAttribute"u8); + public static SingleValueAttributeDescription UnbreakerReplaceAttribute { get; } = new(CompatUnbreakerAttributesNamespace, "UnbreakerReplaceAttribute"u8); + public static SingleValueAttributeDescription UnbreakerShimAttribute { get; } = new(CompatUnbreakerAttributesNamespace, "UnbreakerShimAttribute"u8); +} + +internal abstract class AttributeDescription(Utf8String @namespace, Utf8String name) +{ + public Utf8String Namespace { get; } = @namespace; + + public Utf8String Name { get; } = name; + + public abstract TData CreateData(CustomAttribute customAttribute); +} + +internal sealed class MarkerAttributeDescription(Utf8String @namespace, Utf8String name) : AttributeDescription(@namespace, name) +{ + public override CustomAttribute CreateData(CustomAttribute customAttribute) => customAttribute; +} + +internal sealed class SingleValueAttributeDescription(Utf8String @namespace, Utf8String name) : AttributeDescription(@namespace, name) +{ + public override T CreateData(CustomAttribute customAttribute) + { + ArgumentNullException.ThrowIfNull(customAttribute.Signature); + + var argument = customAttribute.Signature.FixedArguments.Single(); + var value = argument.ArgumentType.ElementType == ElementType.SzArray + ? argument.Elements + : argument.Element; + + return (T) value!; + } +} diff --git a/CompatUnbreaker/Models/Attributes/AttributeDescriptionExtensions.cs b/CompatUnbreaker/Models/Attributes/AttributeDescriptionExtensions.cs new file mode 100644 index 0000000..9f4dba2 --- /dev/null +++ b/CompatUnbreaker/Models/Attributes/AttributeDescriptionExtensions.cs @@ -0,0 +1,54 @@ +using System.Diagnostics.CodeAnalysis; +using AsmResolver.DotNet; + +namespace CompatUnbreaker.Models.Attributes; + +internal static class AttributeDescriptionExtensions +{ + public static IEnumerable Find(this IHasCustomAttribute provider, AttributeDescription attributeDescription) + { + foreach (var attribute in provider.CustomAttributes) + { + var declaringType = attribute.Type; + if (declaringType is null) + continue; + + if (declaringType.Namespace == attributeDescription.Namespace && declaringType.Name == attributeDescription.Name) + { + yield return attributeDescription.CreateData(attribute); + } + } + } + + public static bool Has(this IHasCustomAttribute provider, AttributeDescription attributeDescription) + { + return provider.Find(attributeDescription).Any(); + } + + public static bool TryFindSingle(this IHasCustomAttribute provider, AttributeDescription attributeDescription, [MaybeNullWhen(false)] out TData result) + { + return provider.Find(attributeDescription).TryGetSingle(out result); + } + + private static bool TryGetSingle(this IEnumerable source, [MaybeNullWhen(false)] out TSource result) + { + ArgumentNullException.ThrowIfNull(source); + + using (var e = source.GetEnumerator()) + { + if (!e.MoveNext()) + { + result = default; + return false; + } + + result = e.Current; + if (!e.MoveNext()) + { + return true; + } + } + + throw new InvalidOperationException("Sequence contains more than one element"); + } +} diff --git a/CompatUnbreaker/Models/Attributes/UnbreakerRenameAttributeDescription.cs b/CompatUnbreaker/Models/Attributes/UnbreakerRenameAttributeDescription.cs new file mode 100644 index 0000000..6c5e7d4 --- /dev/null +++ b/CompatUnbreaker/Models/Attributes/UnbreakerRenameAttributeDescription.cs @@ -0,0 +1,62 @@ +using System.Diagnostics; +using AsmResolver; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using CompatUnbreaker.Utilities.AsmResolver; + +namespace CompatUnbreaker.Models.Attributes; + +internal sealed class UnbreakerRenameAttributeDescription(Utf8String @namespace, Utf8String name) + : AttributeDescription(@namespace, name) +{ + public override RenameData CreateData(CustomAttribute customAttribute) + { + ArgumentNullException.ThrowIfNull(customAttribute.Constructor); + ArgumentNullException.ThrowIfNull(customAttribute.Constructor.ContextModule); + ArgumentNullException.ThrowIfNull(customAttribute.Signature); + + var typeFactory = customAttribute.Constructor.ContextModule.CorLibTypeFactory; + + if (CheckSignature(typeFactory.Type(), typeFactory.String)) + { + var arguments = customAttribute.Signature.FixedArguments; + Debug.Assert(arguments.Count == 2); + + return new RenameData.TypeRename( + (TypeSignature) arguments[0].Element!, + (Utf8String) arguments[1].Element! + ); + } + + if (CheckSignature(typeFactory.Type(), typeFactory.String, typeFactory.String)) + { + var arguments = customAttribute.Signature.FixedArguments; + Debug.Assert(arguments.Count == 3); + + return new RenameData.MemberRename( + (TypeSignature) arguments[0].Element!, + (Utf8String) arguments[1].Element!, + (Utf8String) arguments[2].Element! + ); + } + + throw new ArgumentException($"Invalid signature for '{customAttribute}'.", nameof(customAttribute)); + + bool CheckSignature(params ITypeDescriptor[] parameterTypes) + { + return SignatureComparer.Default.Equals( + customAttribute.Constructor?.Signature?.ParameterTypes, + parameterTypes.Select(t => t.ToTypeSignature()) + ); + } + } +} + +internal abstract record RenameData +{ + public sealed record NamespaceRename(Utf8String NamespaceName, Utf8String NewNamespaceName) : RenameData; + + public sealed record TypeRename(TypeSignature Type, Utf8String NewTypeName) : RenameData; + + public sealed record MemberRename(TypeSignature Type, Utf8String MemberName, Utf8String NewMemberName) : RenameData; +} diff --git a/CompatUnbreaker/Models/ShimModel.cs b/CompatUnbreaker/Models/ShimModel.cs new file mode 100644 index 0000000..bf5aa2f --- /dev/null +++ b/CompatUnbreaker/Models/ShimModel.cs @@ -0,0 +1,310 @@ +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using CompatUnbreaker.Attributes; +using CompatUnbreaker.Models.Attributes; +using CompatUnbreaker.Utilities.AsmResolver; + +namespace CompatUnbreaker.Models; + +internal sealed class ShimModel +{ + public required AssemblyDefinition ShimAssembly { get; init; } + public required AssemblyDefinition TargetAssembly { get; init; } + + public required RenameData[] Renames { get; init; } + public required ShimTypeModel[] AllTypes { get; init; } + public required Dictionary ExtensionImplementations { get; init; } + + public static ShimModel From(AssemblyDefinition shimAssembly) + { + var shimModule = shimAssembly.Modules.Single(); + + var targetAssemblyName = shimAssembly.Find(AttributeDescription.UnbreakerShimAttribute).SingleOrDefault() + ?? throw new ArgumentException("Provided shim assembly doesn't have the required UnbreakerShimAttribute.", nameof(shimAssembly)); + + var targetAssemblyReference = shimModule.AssemblyReferences.SingleOrDefault(a => a.Name == targetAssemblyName) + ?? new AssemblyReference(targetAssemblyName, new Version()); + + var targetAssembly = shimModule.MetadataResolver.AssemblyResolver.Resolve(targetAssemblyReference) + ?? throw new ArgumentException($"Could not resolve target assembly '{targetAssemblyReference}'."); + + var targetModule = targetAssembly.Modules.Single(); + + var renames = shimAssembly.Find(AttributeDescription.UnbreakerRenameAttribute).ToArray(); + + var extensionImplementations = new Dictionary(SignatureComparer.Default); + + var typeModels = new List(); + + var queue = new Queue<(ShimTypeModel? DeclaringType, TypeDefinition Type)>( + shimModule.TopLevelTypes.Select(t => ((ShimTypeModel?) null, t)) + ); + + while (queue.Count > 0) + { + var (declaringType, shimType) = queue.Dequeue(); + + var hasExtensionsAttribute = shimType.Has(AttributeDescription.UnbreakerExtensionsAttribute); + if (hasExtensionsAttribute) + { + foreach (var shimNestedType in shimType.NestedTypes) + { + queue.Enqueue((null, shimNestedType)); + } + + continue; + } + + ShimTypeKind kind; + ITypeDefOrRef targetReference; + TypeSignature? extensionParameter = null; + + if (shimType.TryGetExtensionMarkerMethod() is { } markerMethod && shimType.DeclaringType.Has(AttributeDescription.UnbreakerExtensionsAttribute)) + { + kind = ShimTypeKind.NativeExtension; + extensionParameter = markerMethod.Signature!.ParameterTypes.Single(); + targetReference = extensionParameter.GetUnderlyingTypeDefOrRef(); + // TODO validate target type generics match shim type generics 1 to 1 + } + else if (shimType.TryFindSingle(AttributeDescription.UnbreakerExtensionAttribute, out var extensionAttributeTarget)) + { + kind = ShimTypeKind.UnbreakerExtension; + targetReference = ((TypeDefOrRefSignature) extensionAttributeTarget!).Type; + } + else if (shimType.TryFindSingle(AttributeDescription.UnbreakerReplaceAttribute, out var replaceAttributeTarget)) + { + kind = ShimTypeKind.Replace; + targetReference = ((TypeDefOrRefSignature) replaceAttributeTarget!).Type; + } + else + { + kind = ShimTypeKind.New; + + IResolutionScope scope = declaringType == null + ? targetModule + : declaringType.TargetDescriptor switch + { + TypeReference reference => reference, + TypeDefinition definition => definition.ToTypeReference(), + _ => throw new ArgumentOutOfRangeException(), + }; + + targetReference = new TypeReference(shimModule, scope, shimType.Namespace, shimType.Name); + } + + if (!shimType.IsVisibleOutsideOfAssembly()) + { + if (kind == ShimTypeKind.New) + { + continue; + } + + throw new InvalidOperationException($"Shim type '{shimType.FullName}' isn't public."); + } + + if (targetReference.Scope?.GetAssembly()?.Name != targetAssembly.Name) + { + throw new InvalidOperationException($"Shim type '{shimType.FullName}' targets type '{targetReference}', which is not in the target assembly."); + } + + var targetType = targetReference.Resolve(); + if (targetType != null && !targetType.IsVisibleOutsideOfAssembly()) + { + targetType = null; + } + + if (kind == ShimTypeKind.New && targetType != null) + { + throw new InvalidOperationException($"Shim type '{shimType.FullName}' conflicts with target type and doesn't specify the {nameof(UnbreakerReplaceAttribute)}."); + } + + if (kind != ShimTypeKind.New && targetType == null) + { + throw new InvalidOperationException($"Shim type '{shimType.FullName}' target type '{targetReference}' could not be resolved."); + } + + if (kind == ShimTypeKind.UnbreakerExtension && !shimType.IsStatic()) + { + throw new InvalidOperationException($"Extension shim type '{shimType.FullName}' is not static."); + } + + var members = new List(); + + var ignoredMethods = new HashSet(); + + foreach (var shimProperty in shimType.Properties) + { + if (!shimProperty.IsVisibleOutsideOfAssembly()) continue; + + var hasFieldAttribute = shimProperty.Has(AttributeDescription.UnbreakerFieldAttribute); + + if (hasFieldAttribute) + { + var fieldType = shimProperty.Signature!.ReturnType; + members.Add(new ShimFieldModel.FromProperty + { + TargetDescriptor = new MemberReference(targetReference, shimProperty.Name, new FieldSignature(fieldType)), + Definition = shimProperty, + }); + + if (shimProperty.GetMethod != null) ignoredMethods.Add(shimProperty.GetMethod); + if (shimProperty.SetMethod != null) ignoredMethods.Add(shimProperty.SetMethod); + } + else + { + members.Add(new ShimPropertyModel + { + Definition = shimProperty, + }); + } + } + + foreach (var shimEvent in shimType.Events) + { + if (!shimEvent.IsVisibleOutsideOfAssembly()) continue; + + members.Add(new ShimEventModel + { + Definition = shimEvent, + }); + } + + foreach (var shimMethod in shimType.Methods) + { + if (!shimMethod.IsVisibleOutsideOfAssembly()) continue; + + if (extensionParameter != null) + { + var implementationMethod = shimType.FindCorrespondingExtensionImplementationMethod(shimMethod, extensionParameter) + ?? throw new InvalidOperationException($"Couldn't find corresponding implementation method for {shimMethod}."); + + extensionImplementations.Add(shimMethod, implementationMethod); + } + + if (ignoredMethods.Contains(shimMethod)) continue; + + var isConstructor = shimMethod.Has(AttributeDescription.UnbreakerConstructorAttribute); + + members.Add(new ShimMethodModel + { + IsUnbreakerConstructor = isConstructor, + TargetDescriptor = isConstructor + ? new MemberReference(targetReference, ".ctor"u8, MethodSignature.CreateInstance(targetReference.ContextModule.CorLibTypeFactory.Void, shimMethod.Signature.ParameterTypes)) + : new MemberReference(targetReference, shimMethod.Name, shimMethod.Signature), + Definition = shimMethod, + }); + } + + foreach (var shimField in shimType.Fields) + { + if (!shimField.IsVisibleOutsideOfAssembly()) continue; + + members.Add(new ShimFieldModel.FromField + { + TargetDescriptor = new MemberReference(targetReference, shimField.Name, shimField.Signature), + Definition = shimField, + }); + } + + var typeModel = new ShimTypeModel + { + Kind = kind, + DeclaringType = declaringType, + TargetDescriptor = targetReference, + Members = members.ToArray(), + Definition = shimType, + }; + + typeModels.Add(typeModel); + + foreach (var shimNestedType in shimType.NestedTypes) + { + queue.Enqueue((typeModel, shimNestedType)); + } + } + + return new ShimModel + { + ShimAssembly = shimAssembly, + TargetAssembly = targetAssembly, + Renames = renames, + AllTypes = typeModels.ToArray(), + ExtensionImplementations = extensionImplementations, + }; + } +} + +internal enum ShimTypeKind +{ + New, + Replace, + NativeExtension, + UnbreakerExtension, +} + +internal interface IShimMemberModel +{ + IMemberDescriptor TargetDescriptor { get; } +} + +internal sealed class ShimTypeModel : IShimMemberModel +{ + public required ShimTypeKind Kind { get; init; } + + public required ShimTypeModel? DeclaringType { get; init; } + public required ITypeDefOrRef TargetDescriptor { get; init; } + IMemberDescriptor IShimMemberModel.TargetDescriptor => TargetDescriptor; + + public required IShimMemberModel[] Members { get; init; } + + public IEnumerable Methods => Members.OfType(); + public IEnumerable Fields => Members.OfType(); + public IEnumerable Properties => Members.OfType(); + public IEnumerable Events => Members.OfType(); + + public required TypeDefinition Definition { get; init; } + + public override string ToString() + { + return $"{Kind} : {TargetDescriptor}"; + } +} + +internal sealed class ShimMethodModel : IShimMemberModel +{ + public required MemberReference TargetDescriptor { get; init; } + IMemberDescriptor IShimMemberModel.TargetDescriptor => TargetDescriptor; + + public required bool IsUnbreakerConstructor { get; init; } + public required MethodDefinition Definition { get; init; } +} + +internal abstract class ShimFieldModel : IShimMemberModel +{ + public required MemberReference TargetDescriptor { get; init; } + IMemberDescriptor IShimMemberModel.TargetDescriptor => TargetDescriptor; + + internal sealed class FromField : ShimFieldModel + { + public required FieldDefinition Definition { get; init; } + } + + internal sealed class FromProperty : ShimFieldModel + { + public required PropertyDefinition Definition { get; init; } + } +} + +internal sealed class ShimPropertyModel : IShimMemberModel +{ + IMemberDescriptor IShimMemberModel.TargetDescriptor => throw new NotSupportedException(); + + public required PropertyDefinition Definition { get; init; } +} + +internal sealed class ShimEventModel : IShimMemberModel +{ + IMemberDescriptor IShimMemberModel.TargetDescriptor => throw new NotSupportedException(); + + public required EventDefinition Definition { get; init; } +} diff --git a/CompatUnbreaker/Processors/Abstractions/IConsumerProcessor.cs b/CompatUnbreaker/Processors/Abstractions/IConsumerProcessor.cs new file mode 100644 index 0000000..a24b769 --- /dev/null +++ b/CompatUnbreaker/Processors/Abstractions/IConsumerProcessor.cs @@ -0,0 +1,8 @@ +using AsmResolver.DotNet; + +namespace CompatUnbreaker.Processors.Abstractions; + +internal interface IConsumerProcessor +{ + void Process(ProcessorContext context, AssemblyDefinition consumerAssembly); +} diff --git a/CompatUnbreaker/Processors/Abstractions/IReferenceProcessor.cs b/CompatUnbreaker/Processors/Abstractions/IReferenceProcessor.cs new file mode 100644 index 0000000..2ad6f11 --- /dev/null +++ b/CompatUnbreaker/Processors/Abstractions/IReferenceProcessor.cs @@ -0,0 +1,8 @@ +using AsmResolver.DotNet; + +namespace CompatUnbreaker.Processors.Abstractions; + +internal interface IReferenceProcessor +{ + void Process(ProcessorContext context, AssemblyDefinition referenceAssembly); +} diff --git a/CompatUnbreaker/Processors/Abstractions/ProcessorContext.cs b/CompatUnbreaker/Processors/Abstractions/ProcessorContext.cs new file mode 100644 index 0000000..904f1ee --- /dev/null +++ b/CompatUnbreaker/Processors/Abstractions/ProcessorContext.cs @@ -0,0 +1,8 @@ +using CompatUnbreaker.Models; + +namespace CompatUnbreaker.Processors.Abstractions; + +internal sealed class ProcessorContext +{ + public required ShimModel ShimModel { get; init; } +} diff --git a/CompatUnbreaker/Processors/UnbreakerConsumerProcessor.cs b/CompatUnbreaker/Processors/UnbreakerConsumerProcessor.cs new file mode 100644 index 0000000..26d623d --- /dev/null +++ b/CompatUnbreaker/Processors/UnbreakerConsumerProcessor.cs @@ -0,0 +1,389 @@ +using System.Diagnostics; +using AsmResolver; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Code.Cil; +using AsmResolver.DotNet.Signatures; +using AsmResolver.PE.DotNet.Cil; +using CompatUnbreaker.Models; +using CompatUnbreaker.Models.Attributes; +using CompatUnbreaker.Processors.Abstractions; +using CompatUnbreaker.Utilities.AsmResolver; + +namespace CompatUnbreaker.Processors; + +internal sealed class UnbreakerConsumerProcessor : IConsumerProcessor +{ + public void Process(ProcessorContext context, AssemblyDefinition consumerAssembly) + { + var shimModel = context.ShimModel; + var consumerModule = consumerAssembly.ManifestModule!; + + var memberRenames = shimModel.Renames.OfType() + .GroupBy(r => r.Type, SignatureComparer.Default) + .ToDictionary( + x => x.Key.ToTypeDefOrRef(), + g => g.ToDictionary(r => r.MemberName, r => r.NewMemberName), + SignatureComparer.Default + ); + + var shimMembers = new Dictionary(SignatureComparer.VersionAgnostic); + + foreach (var type in shimModel.AllTypes) + { + if (type.Kind is ShimTypeKind.New or ShimTypeKind.Replace) + { + shimMembers.Add(type.TargetDescriptor, type); + } + + foreach (var member in type.Members) + { + if (member is ShimPropertyModel or ShimEventModel) continue; + shimMembers.Add(member.TargetDescriptor, member); + } + } + + var shimmingImporter = new ShimmingReferenceImporter(consumerModule, shimModel, shimMembers, memberRenames); + var memberShimmer = new MemberShimmer(shimmingImporter, shimMembers, memberRenames); + + foreach (var type in consumerModule.GetAllTypes()) + { + memberShimmer.Visit(type); + } + } +} + +internal sealed class MemberShimmer( + ShimmingReferenceImporter importer, + Dictionary shimMembers, + Dictionary> memberRenames +) : ShimmerImporter(importer) +{ + protected override void VisitMethod(MethodDefinition method) + { + if (method is { IsVirtual: true, IsNewSlot: false }) + { + var baseType = method.DeclaringType!; + + while (true) + { + baseType = baseType.BaseType?.Resolve(); + if (baseType == null) break; + + if (memberRenames.TryGetValue(baseType, out var renames)) + { + if (renames.TryGetValue(method.Name, out var newName)) + { + method.Name = newName; + } + } + } + } + + base.VisitMethod(method); + } + + protected override void VisitCilInstruction(CilMethodBody body, int index, CilInstruction instruction) + { + var instructions = body.Instructions; + + if (instruction.OpCode.OperandType is CilOperandType.InlineField && instruction.Operand is IFieldDescriptor field) + { + var opCode = instruction.OpCode.Code; + + if (shimMembers.TryGetValue(field, out var shimMember)) + { + var shimField = (ShimFieldModel) shimMember; + + switch (shimField) + { + case ShimFieldModel.FromField fromField: + { + instruction.Operand = fromField.Definition.ImportWith(importer); + break; + } + + case ShimFieldModel.FromProperty fromProperty: + { + var accessor = opCode switch + { + CilCode.Ldfld or CilCode.Ldsfld or CilCode.Ldflda or CilCode.Ldsflda => fromProperty.Definition.GetMethod, + CilCode.Stfld or CilCode.Stsfld => fromProperty.Definition.SetMethod, + _ => throw new ArgumentOutOfRangeException(), + }; + + instruction.ReplaceWith(CilOpCodes.Call, accessor.ImportWith(importer)); + + // Create a temporary local for getting dummy field address + if (opCode is CilCode.Ldflda or CilCode.Ldsflda) + { + var temporaryLocal = new CilLocalVariable(field.Signature.FieldType.ImportWith(importer)); + body.LocalVariables.Add(temporaryLocal); + instructions.Insert(++index, CilOpCodes.Stloc, temporaryLocal); + instructions.Insert(++index, CilOpCodes.Ldloca, temporaryLocal); + + // TODO we could have a simple heuristic here that warns if the next instruction is not a call to a readonly method + } + + break; + } + + default: + throw new ArgumentOutOfRangeException(); + } + + return; + } + } + + base.VisitCilInstruction(body, index, instruction); + } +} + +internal sealed class ShimmingReferenceImporter( + ModuleDefinition module, + ShimModel shimModel, + Dictionary shimMembers, + Dictionary> memberRenames +) : ReferenceVisitor(module) +{ + public override IMethodDefOrRef ImportMethod(IMethodDefOrRef method) + { + ArgumentNullException.ThrowIfNull(method); + if (method.DeclaringType is null) + throw new ArgumentException("Cannot import a method that is not added to a type."); + if (method.Signature is null) + throw new ArgumentException("Cannot import a method that does not have a signature."); + + if (shimModel.ExtensionImplementations.TryGetValue(method, out var implementation)) + { + return ImportMethod(implementation); + } + + var signature = ImportMethodSignature(method.Signature); + + var name = method.Name; + ArgumentNullException.ThrowIfNull(name); + + if (shimMembers.TryGetValue(new MemberReference(method.DeclaringType, name, signature), out var shimMember)) + { + return ImportMethod(((ShimMethodModel) shimMember).Definition); + } + + if (memberRenames.TryGetValue(method.DeclaringType.ToTypeSignature().GetUnderlyingTypeDefOrRef(), out var renames) && + renames.TryGetValue(name, out var newName)) + { + name = newName; + } + + return new MemberReference(ImportType(method.DeclaringType), name, signature); + } + + public override MethodSpecification ImportMethod(MethodSpecification method) + { + if (shimModel.ExtensionImplementations.TryGetValue(method.Method, out var implementation)) + { + throw new NotImplementedException(); + return base.ImportMethod(method); + } + + return base.ImportMethod(method); + } + + protected override ITypeDefOrRef ImportType(TypeReference type) + { + if (shimMembers.TryGetValue(type, out var shimMember)) + { + return base.ImportType(((ShimTypeModel) shimMember).Definition); + } + + return base.ImportType(type); + } +} + +internal class ShimmerImporter(ReferenceImporter importer) +{ + public virtual void Visit(TypeDefinition type) + { + type.BaseType = type.BaseType?.ImportWith(importer); + + foreach (var implementation in type.Interfaces) + { + implementation.Interface = implementation.Interface?.ImportWith(importer); + VisitCustomAttributes(implementation); + } + + for (var i = 0; i < type.MethodImplementations.Count; i++) + { + var implementation = type.MethodImplementations[i]; + type.MethodImplementations[i] = new MethodImplementation( + implementation.Declaration?.ImportWith(importer), + implementation.Body?.ImportWith(importer) + ); + } + + VisitCustomAttributes(type); + VisitGenericParameters(type); + + foreach (var field in type.Fields) + { + field.Signature = field.Signature?.ImportWith(importer); + VisitCustomAttributes(field); + } + + foreach (var method in type.Methods) + { + VisitMethod(method); + } + + foreach (var property in type.Properties) + { + property.Signature = property.Signature?.ImportWith(importer); + } + + foreach (var @event in type.Events) + { + @event.EventType = @event.EventType?.ImportWith(importer); + } + } + + private void VisitCustomAttributes(IHasCustomAttribute provider) + { + foreach (var attribute in provider.CustomAttributes) + { + attribute.Constructor = (ICustomAttributeType?) attribute.Constructor?.ImportWith(importer); + + if (attribute.Signature is { } signature) + { + foreach (var argument in signature.FixedArguments) + { + VisitCustomAttributeArgument(argument); + } + + foreach (var argument in signature.NamedArguments) + { + VisitCustomAttributeArgument(argument.Argument); + } + } + } + } + + private void VisitCustomAttributeArgument(CustomAttributeArgument argument) + { + argument.ArgumentType = argument.ArgumentType.ImportWith(importer); + for (var i = 0; i < argument.Elements.Count; i++) + { + var element = argument.Elements[i]; + if (element is TypeSignature typeSignature) + { + argument.Elements[i] = typeSignature.ImportWith(importer); + } + } + } + + private void VisitGenericParameters(IHasGenericParameters provider) + { + foreach (var parameter in provider.GenericParameters) + { + foreach (var constraint in parameter.Constraints) + { + constraint.Constraint = constraint.Constraint?.ImportWith(importer); + VisitCustomAttributes(constraint); + } + + VisitCustomAttributes(parameter); + } + } + + protected virtual void VisitMethod(MethodDefinition method) + { + method.Signature = method.Signature?.ImportWith(importer); + + VisitCustomAttributes(method); + VisitGenericParameters(method); + + foreach (var parameterDefinition in method.ParameterDefinitions) + { + VisitCustomAttributes(parameterDefinition); + } + + if (method.CilMethodBody is { } body) + { + foreach (var localVariable in body.LocalVariables) + { + localVariable.VariableType = localVariable.VariableType.ImportWith(importer); + } + + foreach (var exceptionHandler in body.ExceptionHandlers) + { + exceptionHandler.ExceptionType = exceptionHandler.ExceptionType?.ImportWith(importer); + } + + var instructions = body.Instructions; + for (var i = 0; i < instructions.Count; i++) + { + VisitCilInstruction(body, i, instructions[i]); + } + } + } + + protected virtual void VisitCilInstruction(CilMethodBody body, int index, CilInstruction instruction) + { + switch (instruction.OpCode.OperandType) + { + case CilOperandType.InlineField when instruction.Operand is IFieldDescriptor field: + { + instruction.Operand = field.ImportWith(importer); + break; + } + + case CilOperandType.InlineMethod when instruction.Operand is IMethodDescriptor methodDescriptor: + { + var newMethodDescriptor = methodDescriptor.ImportWith(importer); + if (!SignatureComparer.Default.Equals(newMethodDescriptor, methodDescriptor)) + { + if (instruction.OpCode.Code == CilCode.Callvirt && !newMethodDescriptor.Signature!.HasThis) + { + instruction.OpCode = CilOpCodes.Call; + } + + if (instruction.OpCode.Code == CilCode.Newobj && newMethodDescriptor.Name != ".ctor") + { + instruction.OpCode = CilOpCodes.Call; + } + + instruction.Operand = newMethodDescriptor; + } + + break; + } + + case CilOperandType.InlineSig when instruction.Operand is StandAloneSignature standAlone: + { + instruction.Operand = new StandAloneSignature(standAlone.Signature switch + { + MethodSignature signature => signature.ImportWith(importer), + GenericInstanceMethodSignature signature => signature.ImportWith(importer), + _ => throw new ArgumentOutOfRangeException(), + }); + + break; + } + + case CilOperandType.InlineType when instruction.Operand is ITypeDefOrRef typeDescriptor: + { + instruction.Operand = typeDescriptor.ImportWith(importer); + break; + } + + case CilOperandType.InlineTok: + { + if (instruction.Operand is IImportable importable) + { + instruction.Operand = importable.ImportWith(importer); + } + + break; + } + } + } +} diff --git a/CompatUnbreaker/Processors/UnbreakerReferenceProcessor.cs b/CompatUnbreaker/Processors/UnbreakerReferenceProcessor.cs new file mode 100644 index 0000000..0b9d193 --- /dev/null +++ b/CompatUnbreaker/Processors/UnbreakerReferenceProcessor.cs @@ -0,0 +1,247 @@ +using System.Diagnostics; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Code.Cil; +using AsmResolver.DotNet.Signatures; +using AsmResolver.PE.DotNet.Cil; +using AsmResolver.PE.DotNet.Metadata.Tables; +using CompatUnbreaker.Models; +using CompatUnbreaker.Models.Attributes; +using CompatUnbreaker.Processors.Abstractions; +using CompatUnbreaker.Utilities.AsmResolver; + +namespace CompatUnbreaker.Processors; + +internal sealed class UnbreakerReferenceProcessor : IReferenceProcessor +{ + public void Process(ProcessorContext context, AssemblyDefinition referenceAssembly) + { + if (context.ShimModel.TargetAssembly != referenceAssembly) + { + throw new InvalidOperationException("Shim model target assembly does not match the reference assembly."); + } + + var referenceModule = referenceAssembly.ManifestModule!; + + var importer = new RedirectReferenceImporter(referenceModule); + importer.Assemblies.Add(context.ShimModel.ShimAssembly, context.ShimModel.TargetAssembly); + + foreach (var type in context.ShimModel.AllTypes) + { + if (type.Kind == ShimTypeKind.Replace) + { + importer.Types.Add(type.Definition, type.TargetDescriptor); + } + } + + var memberCloner = new MemberClonerLite(importer); + + foreach (var rename in context.ShimModel.Renames.OfType()) + { + var typeDefinition = rename.Type.Resolve(); + + // TODO other members + typeDefinition.Methods.Single(m => m.Name == rename.NewMemberName).Name = rename.MemberName; + } + + var targetTypes = new Dictionary(); + + foreach (var shimTypeModel in context.ShimModel.AllTypes) + { + var targetType = shimTypeModel.TargetDescriptor.Resolve(); + if (targetType != null && !targetType.IsVisibleOutsideOfAssembly()) + { + targetType = null; + } + + var shimType = shimTypeModel.Definition; + + if (shimTypeModel.Kind == ShimTypeKind.Replace && targetType != null) + { + if (targetType.DeclaringType is { } targetDeclaringType) + { + targetDeclaringType.NestedTypes.Remove(targetType); + } + else + { + referenceModule.TopLevelTypes.Remove(targetType); + } + + targetType = null; + } + + if (targetType == null) + { + targetType = memberCloner.CloneType(shimType); + targetType.Namespace = shimTypeModel.TargetDescriptor.Namespace; + targetType.Name = shimTypeModel.TargetDescriptor.Name; + targetTypes.Add(shimTypeModel, targetType); + + if (shimTypeModel.TargetDescriptor.DeclaringType is { } shimDeclaringTypeDescriptor) + { + if (shimTypeModel.DeclaringType != null && targetTypes.TryGetValue(shimTypeModel.DeclaringType, out var shimDeclaringType)) + { + Debug.Assert(SignatureComparer.Default.Equals(shimDeclaringType, shimDeclaringTypeDescriptor)); + } + else + { + shimDeclaringType = shimDeclaringTypeDescriptor.Resolve() + ?? throw new InvalidOperationException($"Could not resolve declaring type for {shimTypeModel}."); + } + + shimDeclaringType.NestedTypes.Add(targetType); + } + else + { + referenceModule.TopLevelTypes.Add(targetType); + } + } + + var clonedMethods = new Dictionary(); + + foreach (var methodModel in shimTypeModel.Methods) + { + var targetMethod = memberCloner.CloneMethod(methodModel.Definition); + targetMethod.Name = methodModel.TargetDescriptor.Name; + targetMethod.Signature = (MethodSignature) ((MethodSignature) methodModel.TargetDescriptor.Signature).ImportWith(importer); + + if (targetMethod.Name == ".ctor") + { + targetMethod.IsSpecialName = targetMethod.IsRuntimeSpecialName = true; + } + + if (methodModel.Definition.CilMethodBody != null) + { + var body = targetMethod.CilMethodBody = new CilMethodBody(); + body.Instructions.Add(CilOpCodes.Ldnull); + body.Instructions.Add(CilOpCodes.Throw); + } + + targetType.Methods.Add(targetMethod); + clonedMethods.Add(methodModel.Definition, targetMethod); + } + + foreach (var shimFieldModel in shimTypeModel.Fields) + { + FieldDefinition targetField; + switch (shimFieldModel) + { + case ShimFieldModel.FromField fromField: + targetField = memberCloner.CloneField(fromField.Definition); + targetField.Name = fromField.TargetDescriptor.Name; + targetField.Signature = ((FieldSignature) fromField.TargetDescriptor.Signature).ImportWith(importer); + break; + + case ShimFieldModel.FromProperty fromProperty: + var shimProperty = fromProperty.Definition; + targetField = new FieldDefinition(fromProperty.TargetDescriptor.Name, default, ((FieldSignature) fromProperty.TargetDescriptor.Signature).ImportWith(importer)) + { + IsPublic = true, + IsInitOnly = shimProperty.SetMethod == null, + }; + break; + + default: + throw new ArgumentOutOfRangeException(nameof(shimFieldModel)); + } + + targetType.Fields.Add(targetField); + } + + foreach (var shimPropertyModel in shimTypeModel.Properties) + { + var targetProperty = memberCloner.CloneProperty(shimPropertyModel.Definition, clonedMethods); + + targetType.Properties.Add(targetProperty); + } + + foreach (var shimEventModel in shimTypeModel.Events) + { + var targetEvent = memberCloner.CloneEvent(shimEventModel.Definition, clonedMethods); + + targetType.Events.Add(targetEvent); + } + } + + Strip(referenceModule); + } + + private static void ConvertExtensionMethodToInstance(MethodDefinition targetMethod) + { + if (!targetMethod.IsStatic) + { + return; + } + + ArgumentNullException.ThrowIfNull(targetMethod.Signature); + + if (targetMethod.GetExtensionAttribute() is { } extensionAttribute) + { + targetMethod.CustomAttributes.Remove(extensionAttribute); + } + + targetMethod.IsStatic = false; + targetMethod.Signature.ParameterTypes.RemoveAt(0); + targetMethod.Signature.HasThis = true; + + if (targetMethod.Parameters.ThisParameter?.Definition is { } thisParameterDefinition) + { + targetMethod.ParameterDefinitions.Remove(thisParameterDefinition); + } + + foreach (var parameterDefinition in targetMethod.ParameterDefinitions) + { + parameterDefinition.Sequence--; + } + + targetMethod.Parameters.PullUpdatesFromMethodSignature(); + } + + private static void Strip(ModuleDefinition module) + { + foreach (var method in module.GetAllTypes().SelectMany(t => t.Methods)) + { + if (method.CilMethodBody != null) + { + var body = method.CilMethodBody = new CilMethodBody(); + body.Instructions.Add(CilOpCodes.Ldnull); + body.Instructions.Add(CilOpCodes.Throw); + } + } + } + + private sealed class RedirectReferenceImporter(ModuleDefinition module) : ReferenceImporter(module) + { + public Dictionary Assemblies { get; } = new(SignatureComparer.VersionAgnostic); + public Dictionary Types { get; } = new(SignatureComparer.VersionAgnostic); + + protected override AssemblyReference ImportAssembly(AssemblyDescriptor assembly) + { + if (Assemblies.TryGetValue(assembly, out var redirected)) + { + assembly = redirected; + } + + return base.ImportAssembly(assembly); + } + + protected override ITypeDefOrRef ImportType(TypeReference type) + { + if (Types.TryGetValue(type, out var redirected)) + { + return base.ImportType(redirected); + } + + return base.ImportType(type); + } + + protected override ITypeDefOrRef ImportType(TypeDefinition type) + { + if (Types.TryGetValue(type, out var redirected)) + { + return base.ImportType(redirected); + } + + return base.ImportType(type); + } + } +} diff --git a/CompatUnbreaker/Unbreaker.cs b/CompatUnbreaker/Unbreaker.cs new file mode 100644 index 0000000..a4ecdb7 --- /dev/null +++ b/CompatUnbreaker/Unbreaker.cs @@ -0,0 +1,29 @@ +using AsmResolver.DotNet; +using CompatUnbreaker.Models; +using CompatUnbreaker.Processors; +using CompatUnbreaker.Processors.Abstractions; + +namespace CompatUnbreaker; + +public static class Unbreaker +{ + public static void ProcessConsumer(AssemblyDefinition shimAssembly, AssemblyDefinition consumerAssembly) + { + var context = new ProcessorContext + { + ShimModel = ShimModel.From(shimAssembly), + }; + + new UnbreakerConsumerProcessor().Process(context, consumerAssembly); + } + + public static void ProcessReference(AssemblyDefinition shimAssembly, AssemblyDefinition referenceAssembly) + { + var context = new ProcessorContext + { + ShimModel = ShimModel.From(shimAssembly), + }; + + new UnbreakerReferenceProcessor().Process(context, referenceAssembly); + } +} diff --git a/CompatUnbreaker/Utilities/AsmResolver/Accessibility.cs b/CompatUnbreaker/Utilities/AsmResolver/Accessibility.cs new file mode 100644 index 0000000..ebc0c5c --- /dev/null +++ b/CompatUnbreaker/Utilities/AsmResolver/Accessibility.cs @@ -0,0 +1,49 @@ +// https://github.com/dotnet/roslyn/blob/c8b5f306d86bc04c59a413ad17b6152663a1e744/src/Compilers/Core/Portable/Symbols/Accessibility.cs + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CompatUnbreaker.Utilities.AsmResolver; + +internal enum Accessibility +{ + /// + /// No accessibility specified. + /// + NotApplicable = 0, + + // DO NOT CHANGE ORDER OF THESE ENUM VALUES + Private = 1, + + /// + /// Only accessible where both protected and internal members are accessible + /// (more restrictive than , and ). + /// + ProtectedAndInternal = 2, + + /// + /// Only accessible where both protected and friend members are accessible + /// (more restrictive than , and ). + /// + ProtectedAndFriend = ProtectedAndInternal, + + Protected = 3, + + Internal = 4, + Friend = Internal, + + /// + /// Accessible wherever either protected or internal members are accessible + /// (less restrictive than , and ). + /// + ProtectedOrInternal = 5, + + /// + /// Accessible wherever either protected or friend members are accessible + /// (less restrictive than , and ). + /// + ProtectedOrFriend = ProtectedOrInternal, + + Public = 6, +} diff --git a/CompatUnbreaker/Utilities/AsmResolver/AccessibilityExtensions.cs b/CompatUnbreaker/Utilities/AsmResolver/AccessibilityExtensions.cs new file mode 100644 index 0000000..9ab1a4f --- /dev/null +++ b/CompatUnbreaker/Utilities/AsmResolver/AccessibilityExtensions.cs @@ -0,0 +1,107 @@ +using AsmResolver.DotNet; +using AsmResolver.PE.DotNet.Metadata.Tables; + +namespace CompatUnbreaker.Utilities.AsmResolver; + +internal static class AccessibilityExtensions +{ + public static Accessibility GetAccessibility(this IMemberDefinition member) + { + return member switch + { + TypeDefinition type => type.GetAccessibility(), + MethodDefinition method => method.GetAccessibility(), + FieldDefinition field => field.GetAccessibility(), + PropertyDefinition property => property.GetAccessibility(), + EventDefinition @event => @event.GetAccessibility(), + _ => throw new ArgumentOutOfRangeException(nameof(member)), + }; + } + + public static Accessibility GetAccessibility(this TypeDefinition type) + { + return (type.Attributes & TypeAttributes.VisibilityMask) switch + { + TypeAttributes.NotPublic => Accessibility.Internal, + TypeAttributes.Public => Accessibility.Public, + TypeAttributes.NestedPublic => Accessibility.Public, + TypeAttributes.NestedPrivate => Accessibility.Private, + TypeAttributes.NestedFamily => Accessibility.Protected, + TypeAttributes.NestedAssembly => Accessibility.Internal, + TypeAttributes.NestedFamilyAndAssembly => Accessibility.ProtectedAndInternal, + TypeAttributes.NestedFamilyOrAssembly => Accessibility.ProtectedOrInternal, + _ => throw new Exception(), + }; + } + + public static Accessibility GetAccessibility(this MethodDefinition method) + { + return (method.Attributes & MethodAttributes.MemberAccessMask) switch + { + MethodAttributes.Private => Accessibility.Private, + MethodAttributes.FamilyAndAssembly => Accessibility.ProtectedAndInternal, + MethodAttributes.Assembly => Accessibility.Internal, + MethodAttributes.Family => Accessibility.Protected, + MethodAttributes.FamilyOrAssembly => Accessibility.ProtectedOrInternal, + MethodAttributes.Public => Accessibility.Public, + _ => throw new Exception(), + }; + } + + public static Accessibility GetAccessibility(this FieldDefinition field) + { + return (field.Attributes & FieldAttributes.FieldAccessMask) switch + { + FieldAttributes.Private => Accessibility.Private, + FieldAttributes.FamilyAndAssembly => Accessibility.ProtectedAndInternal, + FieldAttributes.Assembly => Accessibility.Internal, + FieldAttributes.Family => Accessibility.Protected, + FieldAttributes.FamilyOrAssembly => Accessibility.ProtectedOrInternal, + FieldAttributes.Public => Accessibility.Public, + _ => throw new Exception(), + }; + } + + public static Accessibility GetAccessibility(this PropertyDefinition property) + { + // if (property.IsOverride()) + // { + // // TODO https://github.com/dotnet/roslyn/blob/c3c7ad6a866dd0b857ad14ce683987c39d2b8fe0/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEPropertySymbol.cs#L458-L478 + // } + + return GetAccessibilityFromAccessors(property.GetMethod, property.SetMethod); + } + + public static Accessibility GetAccessibility(this EventDefinition @event) + { + return GetAccessibilityFromAccessors(@event.AddMethod, @event.RemoveMethod); + } + + private static Accessibility GetAccessibilityFromAccessors(MethodDefinition? accessor1, MethodDefinition? accessor2) + { + var accessibility1 = accessor1?.GetAccessibility(); + var accessibility2 = accessor2?.GetAccessibility(); + + if (accessibility1 == null) + { + return accessibility2 ?? Accessibility.NotApplicable; + } + + if (accessibility2 == null) + { + return accessibility1.Value; + } + + return GetAccessibilityFromAccessors(accessibility1.Value, accessibility2.Value); + } + + private static Accessibility GetAccessibilityFromAccessors(Accessibility accessibility1, Accessibility accessibility2) + { + var minAccessibility = (accessibility1 > accessibility2) ? accessibility2 : accessibility1; + var maxAccessibility = (accessibility1 > accessibility2) ? accessibility1 : accessibility2; + + return minAccessibility == Accessibility.Protected && maxAccessibility == Accessibility.Internal + ? Accessibility.ProtectedOrInternal + : maxAccessibility; + } +} diff --git a/CompatUnbreaker/Utilities/AsmResolver/CorLibTypeFactoryExtensions.cs b/CompatUnbreaker/Utilities/AsmResolver/CorLibTypeFactoryExtensions.cs new file mode 100644 index 0000000..af925f8 --- /dev/null +++ b/CompatUnbreaker/Utilities/AsmResolver/CorLibTypeFactoryExtensions.cs @@ -0,0 +1,13 @@ +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; + +namespace CompatUnbreaker.Utilities.AsmResolver; + +internal static class CorLibTypeFactoryExtensions +{ + public static TypeReference Type(this CorLibTypeFactory corLibTypeFactory) + { + var scope = corLibTypeFactory.CorLibScope; + return new TypeReference(scope.ContextModule, scope, "System"u8, "Type"u8); + } +} diff --git a/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.CustomAttributes.cs b/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.CustomAttributes.cs new file mode 100644 index 0000000..8ecc65b --- /dev/null +++ b/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.CustomAttributes.cs @@ -0,0 +1,68 @@ +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; + +namespace CompatUnbreaker.Utilities.AsmResolver; + +internal readonly partial struct MemberClonerLite +{ + private void CloneCustomAttributes(IHasCustomAttribute sourceProvider, IHasCustomAttribute clonedProvider) + { + foreach (var attribute in sourceProvider.CustomAttributes) + clonedProvider.CustomAttributes.Add(CloneCustomAttribute(attribute)); + } + + private CustomAttribute CloneCustomAttribute(CustomAttribute attribute) + { + var clonedSignature = new CustomAttributeSignature(); + + if (attribute.Signature is not null) + { + // Fixed args. + foreach (var argument in attribute.Signature.FixedArguments) + clonedSignature.FixedArguments.Add(CloneCustomAttributeArgument(argument)); + + // Named args. + foreach (var namedArgument in attribute.Signature.NamedArguments) + { + var clonedArgument = new CustomAttributeNamedArgument( + namedArgument.MemberType, + namedArgument.MemberName, + namedArgument.ArgumentType, + CloneCustomAttributeArgument(namedArgument.Argument)); + + clonedSignature.NamedArguments.Add(clonedArgument); + } + } + + var constructor = attribute.Constructor; + if (constructor is null) + { + throw new ArgumentException( + $"Custom attribute of {attribute.Parent} does not have a constructor defined."); + } + + return new CustomAttribute((ICustomAttributeType) constructor.ImportWith(_importer), clonedSignature); + } + + private CustomAttributeArgument CloneCustomAttributeArgument(CustomAttributeArgument argument) + { + var clonedArgument = new CustomAttributeArgument(argument.ArgumentType.ImportWith(_importer)) + { + IsNullArray = argument.IsNullArray, + }; + + // Copy all elements. + for (var i = 0; i < argument.Elements.Count; i++) + clonedArgument.Elements.Add(CloneElement(argument.Elements[i])); + + return clonedArgument; + } + + private object? CloneElement(object? element) + { + if (element is TypeSignature type) + return type.ImportWith(_importer); + + return element; + } +} diff --git a/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.Fields.cs b/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.Fields.cs new file mode 100644 index 0000000..ffb743f --- /dev/null +++ b/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.Fields.cs @@ -0,0 +1,22 @@ +using AsmResolver.DotNet; +using AsmResolver.DotNet.Cloning; + +namespace CompatUnbreaker.Utilities.AsmResolver; + +internal readonly partial struct MemberClonerLite +{ + private static readonly FieldRvaCloner s_fieldRvaCloner = new(); + + public FieldDefinition CloneField(FieldDefinition field) + { + var clonedField = new FieldDefinition(field.Name, field.Attributes, field.Signature?.ImportWith(_importer)); + + CloneCustomAttributes(field, clonedField); + clonedField.ImplementationMap = CloneImplementationMap(field.ImplementationMap); + clonedField.Constant = CloneConstant(field.Constant); + clonedField.FieldRva = s_fieldRvaCloner.CloneFieldRvaData(field); + clonedField.FieldOffset = field.FieldOffset; + + return clonedField; + } +} diff --git a/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.GenericParameters.cs b/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.GenericParameters.cs new file mode 100644 index 0000000..c4ae991 --- /dev/null +++ b/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.GenericParameters.cs @@ -0,0 +1,31 @@ +using AsmResolver.DotNet; + +namespace CompatUnbreaker.Utilities.AsmResolver; + +internal readonly partial struct MemberClonerLite +{ + private void CloneGenericParameters(IHasGenericParameters sourceProvider, IHasGenericParameters clonedProvider) + { + foreach (var parameter in sourceProvider.GenericParameters) + clonedProvider.GenericParameters.Add(CloneGenericParameter(parameter)); + } + + private GenericParameter CloneGenericParameter(GenericParameter parameter) + { + var clonedParameter = new GenericParameter(parameter.Name, parameter.Attributes); + + foreach (var constraint in parameter.Constraints) + clonedParameter.Constraints.Add(CloneGenericParameterConstraint(constraint)); + + CloneCustomAttributes(parameter, clonedParameter); + return clonedParameter; + } + + private GenericParameterConstraint CloneGenericParameterConstraint(GenericParameterConstraint constraint) + { + var clonedConstraint = new GenericParameterConstraint(constraint.Constraint?.ImportWith(_importer)); + + CloneCustomAttributes(constraint, clonedConstraint); + return clonedConstraint; + } +} diff --git a/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.Methods.cs b/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.Methods.cs new file mode 100644 index 0000000..6a5bb3e --- /dev/null +++ b/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.Methods.cs @@ -0,0 +1,41 @@ +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; + +namespace CompatUnbreaker.Utilities.AsmResolver; + +internal readonly partial struct MemberClonerLite +{ + public MethodDefinition CloneMethod(MethodDefinition method) + { + if (method.Name is null) + throw new ArgumentException($"Method {method} has no name."); + if (method.Signature is null) + throw new ArgumentException($"Method {method} has no signature."); + + var clonedMethod = new MethodDefinition(method.Name, method.Attributes, (MethodSignature?) method.Signature?.ImportWith(_importer)) + { + ImplAttributes = method.ImplAttributes, + }; + + clonedMethod.Parameters.PullUpdatesFromMethodSignature(); + + foreach (var parameterDef in method.ParameterDefinitions) + clonedMethod.ParameterDefinitions.Add(CloneParameterDefinition(parameterDef)); + + CloneCustomAttributes(method, clonedMethod); + CloneGenericParameters(method, clonedMethod); + CloneSecurityDeclarations(method, clonedMethod); + + clonedMethod.ImplementationMap = CloneImplementationMap(method.ImplementationMap); + + return clonedMethod; + } + + private ParameterDefinition CloneParameterDefinition(ParameterDefinition parameterDef) + { + var clonedParameterDef = new ParameterDefinition(parameterDef.Sequence, parameterDef.Name, parameterDef.Attributes); + CloneCustomAttributes(parameterDef, clonedParameterDef); + clonedParameterDef.Constant = CloneConstant(parameterDef.Constant); + return clonedParameterDef; + } +} diff --git a/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.SecurityDeclarations.cs b/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.SecurityDeclarations.cs new file mode 100644 index 0000000..e08deb5 --- /dev/null +++ b/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.SecurityDeclarations.cs @@ -0,0 +1,47 @@ +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; + +namespace CompatUnbreaker.Utilities.AsmResolver; + +internal readonly partial struct MemberClonerLite +{ + private void CloneSecurityDeclarations(IHasSecurityDeclaration sourceProvider, IHasSecurityDeclaration clonedProvider) + { + foreach (var declaration in sourceProvider.SecurityDeclarations) + clonedProvider.SecurityDeclarations.Add(CloneSecurityDeclaration(declaration)); + } + + private SecurityDeclaration CloneSecurityDeclaration(SecurityDeclaration declaration) + { + return new(declaration.Action, ClonePermissionSet(declaration.PermissionSet)); + } + + private PermissionSetSignature? ClonePermissionSet(PermissionSetSignature? permissionSet) + { + if (permissionSet is null) + return null; + + var result = new PermissionSetSignature(); + foreach (var attribute in permissionSet.Attributes) + result.Attributes.Add(CloneSecurityAttribute(attribute)); + return result; + } + + private SecurityAttribute CloneSecurityAttribute(SecurityAttribute attribute) + { + var result = new SecurityAttribute(attribute.AttributeType.ImportWith(_importer)); + + foreach (var argument in attribute.NamedArguments) + { + var newArgument = new CustomAttributeNamedArgument( + argument.MemberType, + argument.MemberName, + argument.ArgumentType.ImportWith(_importer), + CloneCustomAttributeArgument(argument.Argument)); + + result.NamedArguments.Add(newArgument); + } + + return result; + } +} diff --git a/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.Semantics.cs b/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.Semantics.cs new file mode 100644 index 0000000..ac685c8 --- /dev/null +++ b/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.Semantics.cs @@ -0,0 +1,46 @@ +using AsmResolver.DotNet; + +namespace CompatUnbreaker.Utilities.AsmResolver; + +internal readonly partial struct MemberClonerLite +{ + public PropertyDefinition CloneProperty(PropertyDefinition property, Dictionary? clonedMethods) + { + var clonedProperty = new PropertyDefinition( + property.Name, + property.Attributes, + property.Signature?.ImportWith(_importer) + ); + + if (clonedMethods != null) CloneSemantics(property, clonedProperty, clonedMethods); + CloneCustomAttributes(property, clonedProperty); + property.Constant = CloneConstant(property.Constant); + + return clonedProperty; + } + + public EventDefinition CloneEvent(EventDefinition @event, Dictionary? clonedMethods) + { + var clonedEvent = new EventDefinition( + @event.Name, + @event.Attributes, + @event.EventType?.ImportWith(_importer) + ); + + if (clonedMethods != null) CloneSemantics(@event, clonedEvent, clonedMethods); + CloneCustomAttributes(@event, clonedEvent); + + return clonedEvent; + } + + private static void CloneSemantics(IHasSemantics semanticsProvider, IHasSemantics clonedProvider, Dictionary clonedMethods) + { + foreach (var semantics in semanticsProvider.Semantics) + { + if (clonedMethods.TryGetValue(semantics.Method!, out var semanticMethod)) + { + clonedProvider.Semantics.Add(new MethodSemantics(semanticMethod, semantics.Attributes)); + } + } + } +} diff --git a/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.Types.cs b/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.Types.cs new file mode 100644 index 0000000..10bfcda --- /dev/null +++ b/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.Types.cs @@ -0,0 +1,42 @@ +using AsmResolver.DotNet; + +namespace CompatUnbreaker.Utilities.AsmResolver; + +internal partial struct MemberClonerLite +{ + public TypeDefinition CloneType(TypeDefinition type) + { + var clonedType = new TypeDefinition(type.Namespace, type.Name, type.Attributes, type.BaseType?.ImportWith(_importer)); + + // Copy interface implementations. + foreach (var implementation in type.Interfaces) + clonedType.Interfaces.Add(CloneInterfaceImplementation(implementation)); + + // Copy method implementations. + foreach (var implementation in type.MethodImplementations) + { + clonedType.MethodImplementations.Add(new MethodImplementation( + implementation.Declaration?.ImportWith(_importer), + implementation.Body?.ImportWith(_importer) + )); + } + + // Clone class layout. + if (type.ClassLayout is { } layout) + clonedType.ClassLayout = new ClassLayout(layout.PackingSize, layout.ClassSize); + + // Clone remaining metadata. + CloneCustomAttributes(type, clonedType); + CloneGenericParameters(type, clonedType); + CloneSecurityDeclarations(type, clonedType); + + return clonedType; + } + + private InterfaceImplementation CloneInterfaceImplementation(InterfaceImplementation implementation) + { + var clonedImplementation = new InterfaceImplementation(implementation.Interface?.ImportWith(_importer)); + CloneCustomAttributes(implementation, clonedImplementation); + return clonedImplementation; + } +} diff --git a/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.cs b/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.cs new file mode 100644 index 0000000..4297bb9 --- /dev/null +++ b/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.cs @@ -0,0 +1,44 @@ +using AsmResolver.DotNet; +using AsmResolver.DotNet.Cloning; +using AsmResolver.DotNet.Signatures; + +namespace CompatUnbreaker.Utilities.AsmResolver; + +/// +/// Methods extracted from to allow for more flexibility. +/// +internal readonly partial struct MemberClonerLite +{ + private readonly ReferenceImporter _importer; + + public MemberClonerLite(ReferenceImporter importer) + { + _importer = importer; + } + + public MemberClonerLite(ModuleDefinition targetModule) : this(targetModule.DefaultImporter) + { + } + + private ImplementationMap? CloneImplementationMap(ImplementationMap? map) + { + if (map is null) + return null; + if (map.Scope is null) + throw new ArgumentException($"Scope of implementation map {map} is null."); + + return new ImplementationMap(map.Scope.ImportWith(_importer), map.Name, map.Attributes); + } + + private static Constant? CloneConstant(Constant? constant) + { + return constant is not null + ? new Constant( + constant.Type, + constant.Value is null + ? null + : new DataBlobSignature(constant.Value.Data) + ) + : null; + } +} diff --git a/CompatUnbreaker/Utilities/AsmResolver/MemberDefinitionExtensions.cs b/CompatUnbreaker/Utilities/AsmResolver/MemberDefinitionExtensions.cs new file mode 100644 index 0000000..da79a73 --- /dev/null +++ b/CompatUnbreaker/Utilities/AsmResolver/MemberDefinitionExtensions.cs @@ -0,0 +1,19 @@ +using AsmResolver.DotNet; + +namespace CompatUnbreaker.Utilities.AsmResolver; + +internal static class MemberDefinitionExtensions +{ + public static bool IsStatic(this IMemberDefinition member) + { + return member switch + { + TypeDefinition type => type is { IsSealed: true, IsAbstract: true }, + MethodDefinition method => method.IsStatic, + FieldDefinition field => field.IsStatic, + PropertyDefinition property => property.GetMethod?.IsStatic != false && property.SetMethod?.IsStatic != false, + EventDefinition @event => @event.AddMethod?.IsStatic != false && @event.RemoveMethod?.IsStatic != false, + _ => throw new ArgumentOutOfRangeException(nameof(member)), + }; + } +} diff --git a/CompatUnbreaker/Utilities/AsmResolver/MethodDefinitionExtensions.cs b/CompatUnbreaker/Utilities/AsmResolver/MethodDefinitionExtensions.cs new file mode 100644 index 0000000..6067645 --- /dev/null +++ b/CompatUnbreaker/Utilities/AsmResolver/MethodDefinitionExtensions.cs @@ -0,0 +1,17 @@ +using System.Runtime.CompilerServices; +using AsmResolver.DotNet; + +namespace CompatUnbreaker.Utilities.AsmResolver; + +internal static class MethodDefinitionExtensions +{ + public static bool IsExtension(this MethodDefinition method) + { + return method.GetExtensionAttribute() != null; + } + + public static CustomAttribute? GetExtensionAttribute(this MethodDefinition method) + { + return method.FindCustomAttributes("System.Runtime.CompilerServices", nameof(ExtensionAttribute)).FirstOrDefault(); + } +} diff --git a/CompatUnbreaker/Utilities/AsmResolver/ReferenceVisitor.cs b/CompatUnbreaker/Utilities/AsmResolver/ReferenceVisitor.cs new file mode 100644 index 0000000..b9047e7 --- /dev/null +++ b/CompatUnbreaker/Utilities/AsmResolver/ReferenceVisitor.cs @@ -0,0 +1,146 @@ +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using CompatUnbreaker.Models; + +namespace CompatUnbreaker.Utilities.AsmResolver; + +/// +/// but visits everything regardless of whether it was already imported. +/// +internal abstract class ReferenceVisitor(ModuleDefinition module) : ReferenceImporter(module) +{ + /// + public override IResolutionScope ImportScope(IResolutionScope scope) + { + ArgumentNullException.ThrowIfNull(scope); + + return scope switch + { + AssemblyReference assembly => ImportAssembly(assembly), + TypeReference parentType => (IResolutionScope) ImportType(parentType), + ModuleDefinition moduleDef => ImportAssembly(moduleDef.Assembly ?? throw new ArgumentException("Module is not added to an assembly.")), + ModuleReference moduleRef => ImportModule(moduleRef), + _ => throw new ArgumentOutOfRangeException(nameof(scope)), + }; + } + + /// + public override IImplementation ImportImplementation(IImplementation? implementation) + { + ArgumentNullException.ThrowIfNull(implementation); + + return implementation switch + { + AssemblyReference assembly => ImportAssembly(assembly), + ExportedType type => ImportType(type), + FileReference file => ImportFile(file), + _ => throw new ArgumentOutOfRangeException(nameof(implementation)), + }; + } + + /// + protected override ITypeDefOrRef ImportType(TypeDefinition type) + { + ArgumentNullException.ThrowIfNull(type); + if (((ITypeDescriptor) type).Scope is not { } scope) + throw new ArgumentException("Cannot import a type that has not been added to a module."); + + return new TypeReference( + TargetModule, + ImportScope(scope), + type.Namespace, + type.Name); + } + + /// + protected override ITypeDefOrRef ImportType(TypeReference type) + { + ArgumentNullException.ThrowIfNull(type); + + return new TypeReference( + TargetModule, + type.Scope is not null + ? ImportScope(type.Scope) + : null, + type.Namespace, + type.Name); + } + + /// + protected override ITypeDefOrRef ImportType(TypeSpecification type) + { + ArgumentNullException.ThrowIfNull(type); + if (type.Signature is null) + throw new ArgumentNullException(nameof(type)); + + return new TypeSpecification(ImportTypeSignature(type.Signature)); + } + + /// + public override ExportedType ImportType(ExportedType type) + { + ArgumentNullException.ThrowIfNull(type); + + var result = TargetModule.ExportedTypes.FirstOrDefault(a => SignatureComparer.Default.Equals(a, type)); + + if (result is null) + { + result = new ExportedType(ImportImplementation(type.Implementation), type.Namespace, type.Name); + TargetModule.ExportedTypes.Add(result); + } + + return result; + } + + /// + public override TypeSignature ImportTypeSignature(TypeSignature type) + { + ArgumentNullException.ThrowIfNull(type); + + return type.AcceptVisitor(this); + } + + /// + public override IMethodDefOrRef ImportMethod(IMethodDefOrRef method) + { + ArgumentNullException.ThrowIfNull(method); + if (method.DeclaringType is null) + throw new ArgumentException("Cannot import a method that is not added to a type."); + if (method.Signature is null) + throw new ArgumentException("Cannot import a method that does not have a signature."); + + return new MemberReference( + ImportType(method.DeclaringType), + method.Name, + ImportMethodSignature(method.Signature)); + } + + /// + public override MethodSpecification ImportMethod(MethodSpecification method) + { + if (method.Method is null || method.Signature is null) + throw new ArgumentNullException(nameof(method)); + if (method.DeclaringType is null) + throw new ArgumentException("Cannot import a method that is not added to a type."); + + var memberRef = ImportMethod(method.Method); + var signature = ImportGenericInstanceMethodSignature(method.Signature); + + return new MethodSpecification(memberRef, signature); + } + + /// + public override IFieldDescriptor ImportField(IFieldDescriptor field) + { + ArgumentNullException.ThrowIfNull(field); + if (field.DeclaringType is null) + throw new ArgumentException("Cannot import a field that is not added to a type."); + if (field.Signature is null) + throw new ArgumentException("Cannot import a field that does not have a signature."); + + return new MemberReference( + ImportType((ITypeDefOrRef) field.DeclaringType), + field.Name, + ImportFieldSignature(field.Signature)); + } +} diff --git a/CompatUnbreaker/Utilities/AsmResolver/SimpleAssemblyResolver.cs b/CompatUnbreaker/Utilities/AsmResolver/SimpleAssemblyResolver.cs new file mode 100644 index 0000000..43d1ef0 --- /dev/null +++ b/CompatUnbreaker/Utilities/AsmResolver/SimpleAssemblyResolver.cs @@ -0,0 +1,84 @@ +using System.Collections.Concurrent; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Serialized; +using AsmResolver.DotNet.Signatures; + +namespace CompatUnbreaker.Utilities.AsmResolver; + +public sealed class SimpleAssemblyResolver : IAssemblyResolver +{ + private readonly ConcurrentDictionary _assemblies; + + public SimpleAssemblyResolver( + IEnumerable? assemblies = null, + SignatureComparisonFlags comparisonFlags = SignatureComparisonFlags.VersionAgnostic + ) + { + var signatureComparer = new SignatureComparer(comparisonFlags); + + _assemblies = new ConcurrentDictionary(signatureComparer); + + if (assemblies != null) + { + foreach (var assembly in assemblies) + { + Load(assembly); + } + } + } + + public AssemblyDefinition? Resolve(AssemblyDescriptor assembly) + { + if (_assemblies.TryGetValue(assembly, out var result)) + { + return result; + } + + return null; + } + + public void AddToCache(AssemblyDescriptor descriptor, AssemblyDefinition definition) + { + if (!_assemblies.TryAdd(descriptor, definition)) + { + throw new ArgumentException($"An item with the same key has already been added. Key: {descriptor}"); + } + } + + public bool RemoveFromCache(AssemblyDescriptor descriptor) + { + return _assemblies.TryRemove(descriptor, out _); + } + + public bool HasCached(AssemblyDescriptor descriptor) + { + return _assemblies.ContainsKey(descriptor); + } + + public void ClearCache() + { + _assemblies.Clear(); + } + + public void Add(AssemblyDefinition definition) + { + AddToCache(definition, definition); + } + + public void Load(AssemblyDefinition definition) + { + foreach (var module in definition.Modules) + { + module.MetadataResolver = new DefaultMetadataResolver(this); + } + + Add(definition); + } + + public AssemblyDefinition Load(string filePath) + { + var assemblyDefinition = AssemblyDefinition.FromFile(filePath, new ModuleReaderParameters()); + Load(assemblyDefinition); + return assemblyDefinition; + } +} diff --git a/CompatUnbreaker/Utilities/AsmResolver/TypeDefinitionExtensions.cs b/CompatUnbreaker/Utilities/AsmResolver/TypeDefinitionExtensions.cs new file mode 100644 index 0000000..d864d4b --- /dev/null +++ b/CompatUnbreaker/Utilities/AsmResolver/TypeDefinitionExtensions.cs @@ -0,0 +1,86 @@ +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; + +namespace CompatUnbreaker.Utilities.AsmResolver; + +internal static class TypeDefinitionExtensions +{ + public static MethodDefinition? TryGetExtensionMarkerMethod(this TypeDefinition type) + { + const string ExtensionMarkerMethodName = "$"; + + foreach (var method in type.Methods) + { + if (method.IsSpecialName && method.Name == ExtensionMarkerMethodName) + { + return method; + } + } + + return null; + } + + public static MethodDefinition? FindCorrespondingExtensionImplementationMethod(this TypeDefinition @this, MethodDefinition method, TypeSignature extensionParameter) + { + foreach (var candidate in @this.DeclaringType!.Methods) + { + if (!candidate.IsStatic || candidate.Name != method.Name) + { + continue; + } + + var signature = method.Signature!; + var candidateSignature = candidate.Signature!; + + if (candidateSignature.GenericParameterCount != @this.GenericParameters.Count + signature.GenericParameterCount) + { + continue; + } + + var additionalParameterCount = method.IsStatic ? 0 : 1; + if (candidateSignature.ParameterTypes.Count != additionalParameterCount + signature.ParameterTypes.Count) + { + continue; + } + + var typeMap = new Dictionary(SignatureComparer.Default); + { + var index = 0; + + for (var i = 0; i < @this.GenericParameters.Count; i++) + { + typeMap[new GenericParameterSignature(GenericParameterType.Type, i)] = new GenericParameterSignature(GenericParameterType.Method, index++); + } + + for (var i = 0; i < signature.GenericParameterCount; i++) + { + typeMap[new GenericParameterSignature(GenericParameterType.Method, i)] = new GenericParameterSignature(GenericParameterType.Method, index++); + } + } + + var typeMapVisitor = new TypeMapVisitor(typeMap); + + if (!SignatureComparer.Default.Equals(candidateSignature.ReturnType, signature.ReturnType.AcceptVisitor(typeMapVisitor))) + { + continue; + } + + if (!method.IsStatic && !SignatureComparer.Default.Equals(candidateSignature.ParameterTypes[0], extensionParameter.AcceptVisitor(typeMapVisitor))) + { + continue; + } + + if (!SignatureComparer.Default.Equals( + candidateSignature.ParameterTypes.Skip(additionalParameterCount), + signature.ParameterTypes.Select(s => s.AcceptVisitor(typeMapVisitor)) + )) + { + continue; + } + + return candidate; + } + + return null; + } +} diff --git a/CompatUnbreaker/Utilities/AsmResolver/TypeMapVisitor.cs b/CompatUnbreaker/Utilities/AsmResolver/TypeMapVisitor.cs new file mode 100644 index 0000000..3375f25 --- /dev/null +++ b/CompatUnbreaker/Utilities/AsmResolver/TypeMapVisitor.cs @@ -0,0 +1,127 @@ +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; + +namespace CompatUnbreaker.Utilities.AsmResolver; + +internal sealed class TypeMapVisitor( + Dictionary typeMap +) : ITypeSignatureVisitor +{ + TypeSignature ITypeSignatureVisitor.VisitArrayType(ArrayTypeSignature signature) + { + if (typeMap.TryGetValue(signature, out var mapped)) return mapped; + + var result = new ArrayTypeSignature(signature.BaseType.AcceptVisitor(this)); + foreach (var dimension in signature.Dimensions) + result.Dimensions.Add(new ArrayDimension(dimension.Size, dimension.LowerBound)); + return result; + } + + TypeSignature ITypeSignatureVisitor.VisitBoxedType(BoxedTypeSignature signature) + { + if (typeMap.TryGetValue(signature, out var mapped)) return mapped; + + return new BoxedTypeSignature(signature.BaseType.AcceptVisitor(this)); + } + + TypeSignature ITypeSignatureVisitor.VisitByReferenceType(ByReferenceTypeSignature signature) + { + if (typeMap.TryGetValue(signature, out var mapped)) return mapped; + + return new ByReferenceTypeSignature(signature.BaseType.AcceptVisitor(this)); + } + + TypeSignature ITypeSignatureVisitor.VisitCorLibType(CorLibTypeSignature signature) + { + return typeMap.GetValueOrDefault(signature, signature); + } + + TypeSignature ITypeSignatureVisitor.VisitCustomModifierType(CustomModifierTypeSignature signature) + { + if (typeMap.TryGetValue(signature, out var mapped)) return mapped; + + return new CustomModifierTypeSignature(VisitType(signature.ModifierType), signature.IsRequired, signature.BaseType.AcceptVisitor(this)); + } + + TypeSignature ITypeSignatureVisitor.VisitGenericInstanceType(GenericInstanceTypeSignature signature) + { + if (typeMap.TryGetValue(signature, out var mapped)) return mapped; + + var result = new GenericInstanceTypeSignature(VisitType(signature.GenericType), signature.IsValueType); + foreach (var argument in signature.TypeArguments) + result.TypeArguments.Add(argument.AcceptVisitor(this)); + return result; + } + + TypeSignature ITypeSignatureVisitor.VisitGenericParameter(GenericParameterSignature signature) + { + return typeMap.GetValueOrDefault(signature, signature); + } + + TypeSignature ITypeSignatureVisitor.VisitPinnedType(PinnedTypeSignature signature) + { + if (typeMap.TryGetValue(signature, out var mapped)) return mapped; + + return new PinnedTypeSignature(signature.BaseType.AcceptVisitor(this)); + } + + TypeSignature ITypeSignatureVisitor.VisitPointerType(PointerTypeSignature signature) + { + if (typeMap.TryGetValue(signature, out var mapped)) return mapped; + + return new PointerTypeSignature(signature.BaseType.AcceptVisitor(this)); + } + + TypeSignature ITypeSignatureVisitor.VisitSentinelType(SentinelTypeSignature signature) + { + return typeMap.GetValueOrDefault(signature, signature); + } + + TypeSignature ITypeSignatureVisitor.VisitSzArrayType(SzArrayTypeSignature signature) + { + if (typeMap.TryGetValue(signature, out var mapped)) return mapped; + + return new SzArrayTypeSignature(signature.BaseType.AcceptVisitor(this)); + } + + TypeSignature ITypeSignatureVisitor.VisitTypeDefOrRef(TypeDefOrRefSignature signature) + { + if (typeMap.TryGetValue(signature, out var mapped)) return mapped; + + return new TypeDefOrRefSignature(VisitType(signature.Type), signature.IsValueType); + } + + TypeSignature ITypeSignatureVisitor.VisitFunctionPointerType(FunctionPointerTypeSignature signature) + { + if (typeMap.TryGetValue(signature, out var mapped)) return mapped; + + return new FunctionPointerTypeSignature(VisitMethodSignature(signature.Signature)); + } + + private ITypeDefOrRef VisitType(ITypeDefOrRef type) + { + if (type is TypeSpecification { Signature: { } signature }) + { + return new TypeSpecification(signature.AcceptVisitor(this)); + } + + return type; + } + + private MethodSignature VisitMethodSignature(MethodSignature signature) + { + var parameterTypes = new TypeSignature[signature.ParameterTypes.Count]; + for (var i = 0; i < parameterTypes.Length; i++) + parameterTypes[i] = signature.ParameterTypes[i].AcceptVisitor(this); + + var result = new MethodSignature(signature.Attributes, signature.ReturnType.AcceptVisitor(this), parameterTypes) + { + GenericParameterCount = signature.GenericParameterCount, + }; + + for (var i = 0; i < signature.SentinelParameterTypes.Count; i++) + result.SentinelParameterTypes.Add(signature.SentinelParameterTypes[i].AcceptVisitor(this)); + + return result; + } +} diff --git a/CompatUnbreaker/Utilities/AsmResolver/VisibilityExtensions.cs b/CompatUnbreaker/Utilities/AsmResolver/VisibilityExtensions.cs new file mode 100644 index 0000000..122c553 --- /dev/null +++ b/CompatUnbreaker/Utilities/AsmResolver/VisibilityExtensions.cs @@ -0,0 +1,43 @@ +using AsmResolver.DotNet; + +namespace CompatUnbreaker.Utilities.AsmResolver; + +internal static class VisibilityExtensions +{ + public static bool IsVisibleOutsideOfAssembly( + this IMemberDefinition member, + bool includeInternalSymbols = false, + bool includeEffectivelyPrivateSymbols = false + ) + { + return member.GetAccessibility() switch + { + Accessibility.Public => true, + Accessibility.Protected => includeEffectivelyPrivateSymbols || member.DeclaringType == null || !member.DeclaringType.IsEffectivelySealed(includeInternalSymbols), + Accessibility.ProtectedOrInternal => includeEffectivelyPrivateSymbols || includeInternalSymbols || member.DeclaringType == null || !member.DeclaringType.IsEffectivelySealed(includeInternalSymbols), + Accessibility.ProtectedAndInternal => includeInternalSymbols && (includeEffectivelyPrivateSymbols || member.DeclaringType == null || !member.DeclaringType.IsEffectivelySealed(includeInternalSymbols)), + Accessibility.Private => false, + Accessibility.Internal => includeInternalSymbols, + _ => false, + }; + } + + public static bool IsEffectivelySealed(this TypeDefinition type, bool includeInternalSymbols) + { + return type.IsSealed || !HasVisibleConstructor(type, includeInternalSymbols); + } + + private static bool HasVisibleConstructor(TypeDefinition type, bool includeInternalSymbols) + { + foreach (var method in type.Methods) + { + if (method is not { IsConstructor: true, IsStatic: false }) + continue; + + if (method.IsVisibleOutsideOfAssembly(includeInternalSymbols, includeEffectivelyPrivateSymbols: true)) + return true; + } + + return false; + } +} diff --git a/CompatUnbreaker/packages.lock.json b/CompatUnbreaker/packages.lock.json new file mode 100644 index 0000000..d4ca974 --- /dev/null +++ b/CompatUnbreaker/packages.lock.json @@ -0,0 +1,141 @@ +{ + "version": 1, + "dependencies": { + "net6.0": { + "AsmResolver.DotNet": { + "type": "Direct", + "requested": "[6.0.0-dev, )", + "resolved": "6.0.0-dev", + "dependencies": { + "AsmResolver.PE": "6.0.0-dev" + } + }, + "Meziantou.Analyzer": { + "type": "Direct", + "requested": "[2.0.220, )", + "resolved": "2.0.220", + "contentHash": "LknH5tduuxY6jzPGUIBHrszk1fepyEWAD9Ht7Hq1rrGZ02BKx54ADXzrlXTkLNpnkVzRBHU4bO6HH7GwIeDR+w==" + }, + "Microsoft.CodeAnalysis.PublicApiAnalyzers": { + "type": "Direct", + "requested": "[4.14.0, )", + "resolved": "4.14.0", + "contentHash": "5zBDSa8D1pfn5VyDzRekcYdVx/DGAzwegblVJhzdN0kvPt97TPr12haA6ZG0wS2WgBb1W42GeZJRiRtlNaoY1w==" + }, + "PolySharp": { + "type": "Direct", + "requested": "[1.15.0, )", + "resolved": "1.15.0", + "contentHash": "FbU0El+EEjdpuIX4iDbeS7ki1uzpJPx8vbqOzEtqnl1GZeAGJfq+jCbxeJL2y0EPnUNk8dRnnqR2xnYXg9Tf+g==" + }, + "StyleCop.Analyzers": { + "type": "Direct", + "requested": "[1.2.0-beta.556, )", + "resolved": "1.2.0-beta.556", + "contentHash": "llRPgmA1fhC0I0QyFLEcjvtM2239QzKr/tcnbsjArLMJxJlu0AA5G7Fft0OI30pHF3MW63Gf4aSSsjc5m82J1Q==", + "dependencies": { + "StyleCop.Analyzers.Unstable": "1.2.0.556" + } + }, + "AsmResolver": { + "type": "Transitive", + "resolved": "6.0.0-dev" + }, + "AsmResolver.PE": { + "type": "Transitive", + "resolved": "6.0.0-dev", + "dependencies": { + "AsmResolver.PE.File": "6.0.0-dev" + } + }, + "AsmResolver.PE.File": { + "type": "Transitive", + "resolved": "6.0.0-dev", + "dependencies": { + "AsmResolver": "6.0.0-dev" + } + }, + "StyleCop.Analyzers.Unstable": { + "type": "Transitive", + "resolved": "1.2.0.556", + "contentHash": "zvn9Mqs/ox/83cpYPignI8hJEM2A93s2HkHs8HYMOAQW0PkampyoErAiIyKxgTLqbbad29HX/shv/6LGSjPJNQ==" + }, + "compatunbreaker.attributes": { + "type": "Project", + "dependencies": { + "Meziantou.Analyzer": "[2.0.220, )", + "PolySharp": "[1.15.0, )", + "StyleCop.Analyzers": "[1.2.0-beta.556, )" + } + } + }, + "net9.0": { + "AsmResolver.DotNet": { + "type": "Direct", + "requested": "[6.0.0-dev, )", + "resolved": "6.0.0-dev", + "dependencies": { + "AsmResolver.PE": "6.0.0-dev" + } + }, + "Meziantou.Analyzer": { + "type": "Direct", + "requested": "[2.0.220, )", + "resolved": "2.0.220", + "contentHash": "LknH5tduuxY6jzPGUIBHrszk1fepyEWAD9Ht7Hq1rrGZ02BKx54ADXzrlXTkLNpnkVzRBHU4bO6HH7GwIeDR+w==" + }, + "Microsoft.CodeAnalysis.PublicApiAnalyzers": { + "type": "Direct", + "requested": "[4.14.0, )", + "resolved": "4.14.0", + "contentHash": "5zBDSa8D1pfn5VyDzRekcYdVx/DGAzwegblVJhzdN0kvPt97TPr12haA6ZG0wS2WgBb1W42GeZJRiRtlNaoY1w==" + }, + "PolySharp": { + "type": "Direct", + "requested": "[1.15.0, )", + "resolved": "1.15.0", + "contentHash": "FbU0El+EEjdpuIX4iDbeS7ki1uzpJPx8vbqOzEtqnl1GZeAGJfq+jCbxeJL2y0EPnUNk8dRnnqR2xnYXg9Tf+g==" + }, + "StyleCop.Analyzers": { + "type": "Direct", + "requested": "[1.2.0-beta.556, )", + "resolved": "1.2.0-beta.556", + "contentHash": "llRPgmA1fhC0I0QyFLEcjvtM2239QzKr/tcnbsjArLMJxJlu0AA5G7Fft0OI30pHF3MW63Gf4aSSsjc5m82J1Q==", + "dependencies": { + "StyleCop.Analyzers.Unstable": "1.2.0.556" + } + }, + "AsmResolver": { + "type": "Transitive", + "resolved": "6.0.0-dev" + }, + "AsmResolver.PE": { + "type": "Transitive", + "resolved": "6.0.0-dev", + "dependencies": { + "AsmResolver.PE.File": "6.0.0-dev" + } + }, + "AsmResolver.PE.File": { + "type": "Transitive", + "resolved": "6.0.0-dev", + "dependencies": { + "AsmResolver": "6.0.0-dev" + } + }, + "StyleCop.Analyzers.Unstable": { + "type": "Transitive", + "resolved": "1.2.0.556", + "contentHash": "zvn9Mqs/ox/83cpYPignI8hJEM2A93s2HkHs8HYMOAQW0PkampyoErAiIyKxgTLqbbad29HX/shv/6LGSjPJNQ==" + }, + "compatunbreaker.attributes": { + "type": "Project", + "dependencies": { + "Meziantou.Analyzer": "[2.0.220, )", + "PolySharp": "[1.15.0, )", + "StyleCop.Analyzers": "[1.2.0-beta.556, )" + } + } + } + } +} \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..c17d709 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,16 @@ + + + + + true + $(MSBuildThisFileDirectory)artifacts + + + + js6pak + 2025 js6pak + MIT + https://github.com/js6pak/CompatUnbreaker + git + + diff --git a/PlaygroundLibrary/Directory.Build.props b/PlaygroundLibrary/Directory.Build.props new file mode 100644 index 0000000..00ce6a1 --- /dev/null +++ b/PlaygroundLibrary/Directory.Build.props @@ -0,0 +1,10 @@ + + + + + PlaygroundLibrary + PlaygroundLibrary + net9.0 + true + + diff --git a/PlaygroundLibrary/PlaygroundLibrary.V1.csproj b/PlaygroundLibrary/PlaygroundLibrary.V1.csproj new file mode 100644 index 0000000..67562d7 --- /dev/null +++ b/PlaygroundLibrary/PlaygroundLibrary.V1.csproj @@ -0,0 +1,6 @@ + + + 1.0.0 + V1 + + diff --git a/PlaygroundLibrary/PlaygroundLibrary.V2.csproj b/PlaygroundLibrary/PlaygroundLibrary.V2.csproj new file mode 100644 index 0000000..f78ad0a --- /dev/null +++ b/PlaygroundLibrary/PlaygroundLibrary.V2.csproj @@ -0,0 +1,6 @@ + + + 2.0.0 + V2 + + diff --git a/PlaygroundLibrary/TestClass.cs b/PlaygroundLibrary/TestClass.cs new file mode 100644 index 0000000..c6914a6 --- /dev/null +++ b/PlaygroundLibrary/TestClass.cs @@ -0,0 +1,43 @@ +using System.ComponentModel.DataAnnotations; + +#if V1 +namespace PlaygroundLibrary; + +[Display] +public class TestClass +{ + private readonly int readonlyField; + + [Required] + public int Property { get => throw null!; set => throw null!; } + + public int GetOnlyProperty { get => throw null!; } + public int SetOnlyProperty { set => throw null!; } + public int InitOnlyProperty { get => throw null!; init => throw null!; } + + [Obsolete] + public event Action Event + { + add => throw null!; + remove => throw null!; + } +} + +public struct Struct +{ + public int a; + public readonly int b; +} + +public record Record(int a, int b); + +public readonly record struct StructRecord(int a, int b); + +public ref struct RefStruct +{ + public int a; + public ref int b; + public ref readonly int c; + public readonly ref readonly int d; +} +#endif diff --git a/PlaygroundLibrary/packages.lock.json b/PlaygroundLibrary/packages.lock.json new file mode 100644 index 0000000..8803aad --- /dev/null +++ b/PlaygroundLibrary/packages.lock.json @@ -0,0 +1,33 @@ +{ + "version": 1, + "dependencies": { + "net9.0": { + "Meziantou.Analyzer": { + "type": "Direct", + "requested": "[2.0.220, )", + "resolved": "2.0.220", + "contentHash": "LknH5tduuxY6jzPGUIBHrszk1fepyEWAD9Ht7Hq1rrGZ02BKx54ADXzrlXTkLNpnkVzRBHU4bO6HH7GwIeDR+w==" + }, + "PolySharp": { + "type": "Direct", + "requested": "[1.15.0, )", + "resolved": "1.15.0", + "contentHash": "FbU0El+EEjdpuIX4iDbeS7ki1uzpJPx8vbqOzEtqnl1GZeAGJfq+jCbxeJL2y0EPnUNk8dRnnqR2xnYXg9Tf+g==" + }, + "StyleCop.Analyzers": { + "type": "Direct", + "requested": "[1.2.0-beta.556, )", + "resolved": "1.2.0-beta.556", + "contentHash": "llRPgmA1fhC0I0QyFLEcjvtM2239QzKr/tcnbsjArLMJxJlu0AA5G7Fft0OI30pHF3MW63Gf4aSSsjc5m82J1Q==", + "dependencies": { + "StyleCop.Analyzers.Unstable": "1.2.0.556" + } + }, + "StyleCop.Analyzers.Unstable": { + "type": "Transitive", + "resolved": "1.2.0.556", + "contentHash": "zvn9Mqs/ox/83cpYPignI8hJEM2A93s2HkHs8HYMOAQW0PkampyoErAiIyKxgTLqbbad29HX/shv/6LGSjPJNQ==" + } + } + } +} \ No newline at end of file diff --git a/global.json b/global.json new file mode 100644 index 0000000..93792dc --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "9.0.300", + "rollForward": "latestPatch" + } +} diff --git a/nuget.config b/nuget.config new file mode 100644 index 0000000..07e0d45 --- /dev/null +++ b/nuget.config @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + From c7d64545bfe70e989dc2df43ae274454fdfdbc6a Mon Sep 17 00:00:00 2001 From: DaNike Date: Wed, 5 Nov 2025 20:59:35 -0600 Subject: [PATCH 07/15] Align phyisal repo layout with solution layout --- MonoMod.Backports.slnx | 8 ++++---- src/MonoMod.Backports.Shims/ApiCompat.targets | 10 +++++----- src/MonoMod.Backports.Shims/Directory.Build.targets | 4 ++-- src/MonoMod.Backports/FilterTfms.targets | 4 ++-- .../System/Threading/ThreadLocalEx.cs | 8 ++++---- .../FilterPackagesForRestore.csproj | 0 src/{ => build}/FilterPackagesForRestore/Program.cs | 0 src/{ => build}/GenApiCompatDll/GenApiCompatDll.csproj | 0 src/{ => build}/GenApiCompatDll/Program.cs | 0 .../MonoMod.Backports.Tasks/FilterTfmsTask.cs | 0 .../MonoMod.Backports.Tasks.csproj | 0 src/{ => build}/ShimGen/Directory.Build.props | 0 src/{ => build}/ShimGen/Program.cs | 0 src/{ => build}/ShimGen/SequenceEqualityComparer.cs | 0 src/{ => build}/ShimGen/ShimGen.csproj | 0 15 files changed, 17 insertions(+), 17 deletions(-) rename src/{ => build}/FilterPackagesForRestore/FilterPackagesForRestore.csproj (100%) rename src/{ => build}/FilterPackagesForRestore/Program.cs (100%) rename src/{ => build}/GenApiCompatDll/GenApiCompatDll.csproj (100%) rename src/{ => build}/GenApiCompatDll/Program.cs (100%) rename src/{ => build}/MonoMod.Backports.Tasks/FilterTfmsTask.cs (100%) rename src/{ => build}/MonoMod.Backports.Tasks/MonoMod.Backports.Tasks.csproj (100%) rename src/{ => build}/ShimGen/Directory.Build.props (100%) rename src/{ => build}/ShimGen/Program.cs (100%) rename src/{ => build}/ShimGen/SequenceEqualityComparer.cs (100%) rename src/{ => build}/ShimGen/ShimGen.csproj (100%) diff --git a/MonoMod.Backports.slnx b/MonoMod.Backports.slnx index e82f115..f2de6e6 100644 --- a/MonoMod.Backports.slnx +++ b/MonoMod.Backports.slnx @@ -18,9 +18,9 @@ - - - - + + + + diff --git a/src/MonoMod.Backports.Shims/ApiCompat.targets b/src/MonoMod.Backports.Shims/ApiCompat.targets index 26d296e..a2af8ed 100644 --- a/src/MonoMod.Backports.Shims/ApiCompat.targets +++ b/src/MonoMod.Backports.Shims/ApiCompat.targets @@ -3,13 +3,13 @@ - - - + {{PKGREF}} @@ -84,7 +84,7 @@ - + @@ -93,7 +93,7 @@ - diff --git a/src/MonoMod.Backports.Shims/Directory.Build.targets b/src/MonoMod.Backports.Shims/Directory.Build.targets index 88c58c9..73fdf13 100644 --- a/src/MonoMod.Backports.Shims/Directory.Build.targets +++ b/src/MonoMod.Backports.Shims/Directory.Build.targets @@ -4,7 +4,7 @@ - - diff --git a/src/MonoMod.Backports/FilterTfms.targets b/src/MonoMod.Backports/FilterTfms.targets index f8eff0f..ce719ec 100644 --- a/src/MonoMod.Backports/FilterTfms.targets +++ b/src/MonoMod.Backports/FilterTfms.targets @@ -19,7 +19,7 @@ AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll"> - + @@ -28,7 +28,7 @@ AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll"> - + diff --git a/src/MonoMod.Backports/System/Threading/ThreadLocalEx.cs b/src/MonoMod.Backports/System/Threading/ThreadLocalEx.cs index dee50ef..6e7f393 100644 --- a/src/MonoMod.Backports/System/Threading/ThreadLocalEx.cs +++ b/src/MonoMod.Backports/System/Threading/ThreadLocalEx.cs @@ -64,7 +64,7 @@ public static bool SupportsAllValues #if HAS_ALLVALUES => true; #else - => false; + => ThreadLocalInfo.Info.GetValuesDel is not null; #endif extension(ThreadLocal) @@ -91,7 +91,7 @@ public static ThreadLocal Create(bool trackAllValues) public static ThreadLocal Create(Func valueFactory, bool trackAllValues) { #if HAS_ALLVALUES - return new(trackAllValues); + return new(valueFactory, trackAllValues); #else if (ThreadLocalInfo.Info.CreateFuncBoolDel is { } create) { @@ -132,13 +132,13 @@ public static IList Values(this ThreadLocal threadLocal) public IList Values => self.Values(); } +#if false // when extension constructors actually exist extension(ThreadLocal) { -#if false // when extension constructors actually exist public static ThreadLocal(bool trackAllValues) => Create(trackAllValues); public static ThreadLocal(Func valueFactory, bool trackAllValues) => Create(valueFactory, trackAllValues); -#endif } +#endif } diff --git a/src/FilterPackagesForRestore/FilterPackagesForRestore.csproj b/src/build/FilterPackagesForRestore/FilterPackagesForRestore.csproj similarity index 100% rename from src/FilterPackagesForRestore/FilterPackagesForRestore.csproj rename to src/build/FilterPackagesForRestore/FilterPackagesForRestore.csproj diff --git a/src/FilterPackagesForRestore/Program.cs b/src/build/FilterPackagesForRestore/Program.cs similarity index 100% rename from src/FilterPackagesForRestore/Program.cs rename to src/build/FilterPackagesForRestore/Program.cs diff --git a/src/GenApiCompatDll/GenApiCompatDll.csproj b/src/build/GenApiCompatDll/GenApiCompatDll.csproj similarity index 100% rename from src/GenApiCompatDll/GenApiCompatDll.csproj rename to src/build/GenApiCompatDll/GenApiCompatDll.csproj diff --git a/src/GenApiCompatDll/Program.cs b/src/build/GenApiCompatDll/Program.cs similarity index 100% rename from src/GenApiCompatDll/Program.cs rename to src/build/GenApiCompatDll/Program.cs diff --git a/src/MonoMod.Backports.Tasks/FilterTfmsTask.cs b/src/build/MonoMod.Backports.Tasks/FilterTfmsTask.cs similarity index 100% rename from src/MonoMod.Backports.Tasks/FilterTfmsTask.cs rename to src/build/MonoMod.Backports.Tasks/FilterTfmsTask.cs diff --git a/src/MonoMod.Backports.Tasks/MonoMod.Backports.Tasks.csproj b/src/build/MonoMod.Backports.Tasks/MonoMod.Backports.Tasks.csproj similarity index 100% rename from src/MonoMod.Backports.Tasks/MonoMod.Backports.Tasks.csproj rename to src/build/MonoMod.Backports.Tasks/MonoMod.Backports.Tasks.csproj diff --git a/src/ShimGen/Directory.Build.props b/src/build/ShimGen/Directory.Build.props similarity index 100% rename from src/ShimGen/Directory.Build.props rename to src/build/ShimGen/Directory.Build.props diff --git a/src/ShimGen/Program.cs b/src/build/ShimGen/Program.cs similarity index 100% rename from src/ShimGen/Program.cs rename to src/build/ShimGen/Program.cs diff --git a/src/ShimGen/SequenceEqualityComparer.cs b/src/build/ShimGen/SequenceEqualityComparer.cs similarity index 100% rename from src/ShimGen/SequenceEqualityComparer.cs rename to src/build/ShimGen/SequenceEqualityComparer.cs diff --git a/src/ShimGen/ShimGen.csproj b/src/build/ShimGen/ShimGen.csproj similarity index 100% rename from src/ShimGen/ShimGen.csproj rename to src/build/ShimGen/ShimGen.csproj From fcac0df13a11d1833a08073cca93d3ef2ee1d394 Mon Sep 17 00:00:00 2001 From: DaNike Date: Wed, 5 Nov 2025 21:46:13 -0600 Subject: [PATCH 08/15] Import the CompatUnbreaker ApiCompat stuff as dedicated project --- MonoMod.Backports.slnx | 1 + .../AssemblyMapping/AssemblyMapper.cs | 0 .../AssemblyMapping/ElementMapper.cs | 0 .../AssemblyMapping/ElementSide.cs | 0 .../AssemblyMapping/MapperSettings.cs | 0 .../AssemblyMapping/MemberMapper.cs | 0 .../AssemblyMapping/TypeMapper.cs | 0 .../ApiCompatibility/Comparing/ApiComparer.cs | 0 .../Comparing/CompatDifference.cs | 0 .../Comparing/DifferenceType.cs | 0 .../Comparing/Rules/BaseRule.cs | 0 .../Rules/CannotAddAbstractMember.cs | 2 +- .../Rules/CannotAddMemberToInterface.cs | 6 +- .../Rules/CannotAddOrRemoveVirtualKeyword.cs | 2 +- .../Rules/CannotChangeGenericConstraints.cs | 4 +- .../Comparing/Rules/CannotChangeVisibility.cs | 8 +- .../Rules/CannotRemoveBaseTypeOrInterface.cs | 4 +- .../Comparing/Rules/CannotSealType.cs | 2 +- .../Comparing/Rules/EnumsMustMatch.cs | 2 +- .../Comparing/Rules/MembersMustExist.cs | 0 .../ApiCompatibility/README.md | 0 src/build/ArApiCompat/ArApiCompat.csproj | 13 + .../Utilities/AsmResolver/Accessibility.cs | 0 .../AsmResolver/AccessibilityExtensions.cs | 0 .../DefinitionModifiersExtensions.cs | 0 .../AsmResolver/ExtendedSignatureComparer.cs | 0 .../AsmResolver/GenericParameterExtensions.cs | 0 .../AsmResolver/MemberDefinitionExtensions.cs | 12 + .../AsmResolver/MethodDefinitionExtensions.cs | 0 .../AsmResolver/MiscellaneousExtensions.cs | 0 ...peDefinitionExtensions.InterfaceMapping.cs | 0 .../AsmResolver/TypeDefinitionExtensions.cs | 0 .../AsmResolver/TypeSignatureExtensions.cs | 0 .../AsmResolver/VisibilityExtensions.cs | 0 .../Utilities/MetadataHelpers.cs | 0 .../Utilities/StringExtensions.cs | 0 .../.github/workflows/main.yml | 63 -- src/build/CompatUnbreaker/.gitignore | 23 - .../CompatUnbreaker.Attributes.csproj | 11 - .../NotSupportedAnymoreException.cs | 16 - .../UnbreakerConstructorAttribute.cs | 4 - .../UnbreakerExtensionAttribute.cs | 9 - .../UnbreakerExtensionsAttribute.cs | 4 - .../UnbreakerFieldAttribute.cs | 4 - .../UnbreakerRenameAttribute.cs | 45 -- .../UnbreakerReplaceAttribute.cs | 9 - .../UnbreakerShimAttribute.cs | 9 - .../UnbreakerThisAttribute.cs | 7 - .../packages.lock.json | 117 ---- .../AsmResolverSyntaxGeneratorTests.cs | 172 ----- .../CompatUnbreaker.Tests.csproj | 21 - .../TestFiles/AttributeTests.cs | 80 --- .../TestFiles/DllImportTests.cs | 12 - .../TestFiles/MethodTests.cs | 26 - .../TestFiles/NamedTupleTests.cs | 14 - .../TestFiles/ReservedTests.cs | 28 - .../CompatUnbreaker.Tests/packages.lock.json | 567 ---------------- .../Commands/BaseShimProjectCommand.cs | 112 ---- .../Commands/CompareCommand.cs | 21 - .../Commands/SkeletonCommand.cs | 15 - .../CompatUnbreaker.Tool.csproj | 35 - .../CompatUnbreaker.Tool/Program.cs | 17 - .../AsmResolverTypeSyntaxGenerator.cs | 377 ----------- .../DeclarationModifiersExtensions.cs | 35 - .../CodeGeneration/NullableAnnotation.cs | 8 - .../RoslynIdentifierExtensions.cs | 57 -- .../SyntaxGeneratorExtensions.Attributes.cs | 184 ----- .../SyntaxGeneratorExtensions.EnumValues.cs | 216 ------ .../SyntaxGeneratorExtensions.Events.cs | 49 -- .../SyntaxGeneratorExtensions.Fields.cs | 39 -- .../SyntaxGeneratorExtensions.Generics.cs | 132 ---- .../SyntaxGeneratorExtensions.Methods.cs | 428 ------------ .../SyntaxGeneratorExtensions.Properties.cs | 86 --- .../SyntaxGeneratorExtensions.Types.cs | 209 ------ ...ntaxGeneratorExtensions.UnsafeAccessors.cs | 115 ---- .../SyntaxGeneratorExtensions.cs | 44 -- .../CodeGeneration/TypeContext.cs | 150 ----- .../SkeletonGeneration/MethodBodyRewriter.cs | 50 -- .../SkeletonGeneration/SkeletonGenerator.cs | 632 ------------------ .../Utilities/Roslyn/TypeSymbolExtensions.cs | 62 -- .../CompatUnbreaker.Tool/packages.lock.json | 506 -------------- src/build/CompatUnbreaker/CompatUnbreaker.sln | 52 -- .../CompatUnbreaker/CompatUnbreaker.csproj | 25 - .../Models/Attributes/AttributeDescription.cs | 50 -- .../AttributeDescriptionExtensions.cs | 54 -- .../UnbreakerRenameAttributeDescription.cs | 62 -- .../CompatUnbreaker/Models/ShimModel.cs | 310 --------- .../Abstractions/IConsumerProcessor.cs | 8 - .../Abstractions/IReferenceProcessor.cs | 8 - .../Abstractions/ProcessorContext.cs | 8 - .../Processors/UnbreakerConsumerProcessor.cs | 389 ----------- .../Processors/UnbreakerReferenceProcessor.cs | 247 ------- .../CompatUnbreaker/Unbreaker.cs | 29 - .../CorLibTypeFactoryExtensions.cs | 13 - .../MemberClonerLite.CustomAttributes.cs | 68 -- .../AsmResolver/MemberClonerLite.Fields.cs | 22 - .../MemberClonerLite.GenericParameters.cs | 31 - .../AsmResolver/MemberClonerLite.Methods.cs | 41 -- .../MemberClonerLite.SecurityDeclarations.cs | 47 -- .../AsmResolver/MemberClonerLite.Semantics.cs | 46 -- .../AsmResolver/MemberClonerLite.Types.cs | 42 -- .../Utilities/AsmResolver/MemberClonerLite.cs | 44 -- .../AsmResolver/MemberDefinitionExtensions.cs | 19 - .../AsmResolver/MethodDefinitionExtensions.cs | 17 - .../Utilities/AsmResolver/ReferenceVisitor.cs | 146 ---- .../AsmResolver/SimpleAssemblyResolver.cs | 84 --- .../AsmResolver/TypeDefinitionExtensions.cs | 86 --- .../Utilities/AsmResolver/TypeMapVisitor.cs | 127 ---- .../CompatUnbreaker/packages.lock.json | 141 ---- .../CompatUnbreaker/Directory.Build.props | 16 - .../PlaygroundLibrary/Directory.Build.props | 10 - .../PlaygroundLibrary.V1.csproj | 6 - .../PlaygroundLibrary.V2.csproj | 6 - .../PlaygroundLibrary/TestClass.cs | 43 -- .../PlaygroundLibrary/packages.lock.json | 33 - src/build/CompatUnbreaker/global.json | 6 - src/build/CompatUnbreaker/nuget.config | 18 - 117 files changed, 42 insertions(+), 7188 deletions(-) rename src/build/{CompatUnbreaker/CompatUnbreaker.Tool => ArApiCompat}/ApiCompatibility/AssemblyMapping/AssemblyMapper.cs (100%) rename src/build/{CompatUnbreaker/CompatUnbreaker.Tool => ArApiCompat}/ApiCompatibility/AssemblyMapping/ElementMapper.cs (100%) rename src/build/{CompatUnbreaker/CompatUnbreaker.Tool => ArApiCompat}/ApiCompatibility/AssemblyMapping/ElementSide.cs (100%) rename src/build/{CompatUnbreaker/CompatUnbreaker.Tool => ArApiCompat}/ApiCompatibility/AssemblyMapping/MapperSettings.cs (100%) rename src/build/{CompatUnbreaker/CompatUnbreaker.Tool => ArApiCompat}/ApiCompatibility/AssemblyMapping/MemberMapper.cs (100%) rename src/build/{CompatUnbreaker/CompatUnbreaker.Tool => ArApiCompat}/ApiCompatibility/AssemblyMapping/TypeMapper.cs (100%) rename src/build/{CompatUnbreaker/CompatUnbreaker.Tool => ArApiCompat}/ApiCompatibility/Comparing/ApiComparer.cs (100%) rename src/build/{CompatUnbreaker/CompatUnbreaker.Tool => ArApiCompat}/ApiCompatibility/Comparing/CompatDifference.cs (100%) rename src/build/{CompatUnbreaker/CompatUnbreaker.Tool => ArApiCompat}/ApiCompatibility/Comparing/DifferenceType.cs (100%) rename src/build/{CompatUnbreaker/CompatUnbreaker.Tool => ArApiCompat}/ApiCompatibility/Comparing/Rules/BaseRule.cs (100%) rename src/build/{CompatUnbreaker/CompatUnbreaker.Tool => ArApiCompat}/ApiCompatibility/Comparing/Rules/CannotAddAbstractMember.cs (88%) rename src/build/{CompatUnbreaker/CompatUnbreaker.Tool => ArApiCompat}/ApiCompatibility/Comparing/Rules/CannotAddMemberToInterface.cs (92%) rename src/build/{CompatUnbreaker/CompatUnbreaker.Tool => ArApiCompat}/ApiCompatibility/Comparing/Rules/CannotAddOrRemoveVirtualKeyword.cs (95%) rename src/build/{CompatUnbreaker/CompatUnbreaker.Tool => ArApiCompat}/ApiCompatibility/Comparing/Rules/CannotChangeGenericConstraints.cs (97%) rename src/build/{CompatUnbreaker/CompatUnbreaker.Tool => ArApiCompat}/ApiCompatibility/Comparing/Rules/CannotChangeVisibility.cs (89%) rename src/build/{CompatUnbreaker/CompatUnbreaker.Tool => ArApiCompat}/ApiCompatibility/Comparing/Rules/CannotRemoveBaseTypeOrInterface.cs (95%) rename src/build/{CompatUnbreaker/CompatUnbreaker.Tool => ArApiCompat}/ApiCompatibility/Comparing/Rules/CannotSealType.cs (94%) rename src/build/{CompatUnbreaker/CompatUnbreaker.Tool => ArApiCompat}/ApiCompatibility/Comparing/Rules/EnumsMustMatch.cs (96%) rename src/build/{CompatUnbreaker/CompatUnbreaker.Tool => ArApiCompat}/ApiCompatibility/Comparing/Rules/MembersMustExist.cs (100%) rename src/build/{CompatUnbreaker/CompatUnbreaker.Tool => ArApiCompat}/ApiCompatibility/README.md (100%) create mode 100644 src/build/ArApiCompat/ArApiCompat.csproj rename src/build/{CompatUnbreaker/CompatUnbreaker => ArApiCompat}/Utilities/AsmResolver/Accessibility.cs (100%) rename src/build/{CompatUnbreaker/CompatUnbreaker => ArApiCompat}/Utilities/AsmResolver/AccessibilityExtensions.cs (100%) rename src/build/{CompatUnbreaker/CompatUnbreaker.Tool => ArApiCompat}/Utilities/AsmResolver/DefinitionModifiersExtensions.cs (100%) rename src/build/{CompatUnbreaker/CompatUnbreaker.Tool => ArApiCompat}/Utilities/AsmResolver/ExtendedSignatureComparer.cs (100%) rename src/build/{CompatUnbreaker/CompatUnbreaker.Tool => ArApiCompat}/Utilities/AsmResolver/GenericParameterExtensions.cs (100%) rename src/build/{CompatUnbreaker/CompatUnbreaker.Tool => ArApiCompat}/Utilities/AsmResolver/MemberDefinitionExtensions.cs (87%) rename src/build/{CompatUnbreaker/CompatUnbreaker.Tool => ArApiCompat}/Utilities/AsmResolver/MethodDefinitionExtensions.cs (100%) rename src/build/{CompatUnbreaker/CompatUnbreaker.Tool => ArApiCompat}/Utilities/AsmResolver/MiscellaneousExtensions.cs (100%) rename src/build/{CompatUnbreaker/CompatUnbreaker.Tool => ArApiCompat}/Utilities/AsmResolver/TypeDefinitionExtensions.InterfaceMapping.cs (100%) rename src/build/{CompatUnbreaker/CompatUnbreaker.Tool => ArApiCompat}/Utilities/AsmResolver/TypeDefinitionExtensions.cs (100%) rename src/build/{CompatUnbreaker/CompatUnbreaker.Tool => ArApiCompat}/Utilities/AsmResolver/TypeSignatureExtensions.cs (100%) rename src/build/{CompatUnbreaker/CompatUnbreaker => ArApiCompat}/Utilities/AsmResolver/VisibilityExtensions.cs (100%) rename src/build/{CompatUnbreaker/CompatUnbreaker.Tool => ArApiCompat}/Utilities/MetadataHelpers.cs (100%) rename src/build/{CompatUnbreaker/CompatUnbreaker.Tool => ArApiCompat}/Utilities/StringExtensions.cs (100%) delete mode 100644 src/build/CompatUnbreaker/.github/workflows/main.yml delete mode 100644 src/build/CompatUnbreaker/.gitignore delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker.Attributes/CompatUnbreaker.Attributes.csproj delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker.Attributes/NotSupportedAnymoreException.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker.Attributes/UnbreakerConstructorAttribute.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker.Attributes/UnbreakerExtensionAttribute.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker.Attributes/UnbreakerExtensionsAttribute.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker.Attributes/UnbreakerFieldAttribute.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker.Attributes/UnbreakerRenameAttribute.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker.Attributes/UnbreakerReplaceAttribute.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker.Attributes/UnbreakerShimAttribute.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker.Attributes/UnbreakerThisAttribute.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker.Attributes/packages.lock.json delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker.Tests/AsmResolverSyntaxGeneratorTests.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker.Tests/CompatUnbreaker.Tests.csproj delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker.Tests/TestFiles/AttributeTests.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker.Tests/TestFiles/DllImportTests.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker.Tests/TestFiles/MethodTests.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker.Tests/TestFiles/NamedTupleTests.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker.Tests/TestFiles/ReservedTests.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker.Tests/packages.lock.json delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker.Tool/Commands/BaseShimProjectCommand.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker.Tool/Commands/CompareCommand.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker.Tool/Commands/SkeletonCommand.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker.Tool/CompatUnbreaker.Tool.csproj delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker.Tool/Program.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/AsmResolverTypeSyntaxGenerator.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/DeclarationModifiersExtensions.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/NullableAnnotation.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/RoslynIdentifierExtensions.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Attributes.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.EnumValues.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Events.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Fields.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Generics.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Methods.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Properties.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Types.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.UnsafeAccessors.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/TypeContext.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/MethodBodyRewriter.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/SkeletonGenerator.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker.Tool/Utilities/Roslyn/TypeSymbolExtensions.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker.Tool/packages.lock.json delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker.sln delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker/CompatUnbreaker.csproj delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker/Models/Attributes/AttributeDescription.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker/Models/Attributes/AttributeDescriptionExtensions.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker/Models/Attributes/UnbreakerRenameAttributeDescription.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker/Models/ShimModel.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker/Processors/Abstractions/IConsumerProcessor.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker/Processors/Abstractions/IReferenceProcessor.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker/Processors/Abstractions/ProcessorContext.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker/Processors/UnbreakerConsumerProcessor.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker/Processors/UnbreakerReferenceProcessor.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker/Unbreaker.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/CorLibTypeFactoryExtensions.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.CustomAttributes.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.Fields.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.GenericParameters.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.Methods.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.SecurityDeclarations.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.Semantics.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.Types.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/MemberDefinitionExtensions.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/MethodDefinitionExtensions.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/ReferenceVisitor.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/SimpleAssemblyResolver.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/TypeDefinitionExtensions.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/TypeMapVisitor.cs delete mode 100644 src/build/CompatUnbreaker/CompatUnbreaker/packages.lock.json delete mode 100644 src/build/CompatUnbreaker/Directory.Build.props delete mode 100644 src/build/CompatUnbreaker/PlaygroundLibrary/Directory.Build.props delete mode 100644 src/build/CompatUnbreaker/PlaygroundLibrary/PlaygroundLibrary.V1.csproj delete mode 100644 src/build/CompatUnbreaker/PlaygroundLibrary/PlaygroundLibrary.V2.csproj delete mode 100644 src/build/CompatUnbreaker/PlaygroundLibrary/TestClass.cs delete mode 100644 src/build/CompatUnbreaker/PlaygroundLibrary/packages.lock.json delete mode 100644 src/build/CompatUnbreaker/global.json delete mode 100644 src/build/CompatUnbreaker/nuget.config diff --git a/MonoMod.Backports.slnx b/MonoMod.Backports.slnx index f2de6e6..ca155d6 100644 --- a/MonoMod.Backports.slnx +++ b/MonoMod.Backports.slnx @@ -18,6 +18,7 @@ + diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/AssemblyMapping/AssemblyMapper.cs b/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/AssemblyMapper.cs similarity index 100% rename from src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/AssemblyMapping/AssemblyMapper.cs rename to src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/AssemblyMapper.cs diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/AssemblyMapping/ElementMapper.cs b/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/ElementMapper.cs similarity index 100% rename from src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/AssemblyMapping/ElementMapper.cs rename to src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/ElementMapper.cs diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/AssemblyMapping/ElementSide.cs b/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/ElementSide.cs similarity index 100% rename from src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/AssemblyMapping/ElementSide.cs rename to src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/ElementSide.cs diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/AssemblyMapping/MapperSettings.cs b/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/MapperSettings.cs similarity index 100% rename from src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/AssemblyMapping/MapperSettings.cs rename to src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/MapperSettings.cs diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/AssemblyMapping/MemberMapper.cs b/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/MemberMapper.cs similarity index 100% rename from src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/AssemblyMapping/MemberMapper.cs rename to src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/MemberMapper.cs diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/AssemblyMapping/TypeMapper.cs b/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/TypeMapper.cs similarity index 100% rename from src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/AssemblyMapping/TypeMapper.cs rename to src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/TypeMapper.cs diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/Comparing/ApiComparer.cs b/src/build/ArApiCompat/ApiCompatibility/Comparing/ApiComparer.cs similarity index 100% rename from src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/Comparing/ApiComparer.cs rename to src/build/ArApiCompat/ApiCompatibility/Comparing/ApiComparer.cs diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/Comparing/CompatDifference.cs b/src/build/ArApiCompat/ApiCompatibility/Comparing/CompatDifference.cs similarity index 100% rename from src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/Comparing/CompatDifference.cs rename to src/build/ArApiCompat/ApiCompatibility/Comparing/CompatDifference.cs diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/Comparing/DifferenceType.cs b/src/build/ArApiCompat/ApiCompatibility/Comparing/DifferenceType.cs similarity index 100% rename from src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/Comparing/DifferenceType.cs rename to src/build/ArApiCompat/ApiCompatibility/Comparing/DifferenceType.cs diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/BaseRule.cs b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/BaseRule.cs similarity index 100% rename from src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/BaseRule.cs rename to src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/BaseRule.cs diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotAddAbstractMember.cs b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotAddAbstractMember.cs similarity index 88% rename from src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotAddAbstractMember.cs rename to src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotAddAbstractMember.cs index 8c73aee..dffe6b2 100644 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotAddAbstractMember.cs +++ b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotAddAbstractMember.cs @@ -22,7 +22,7 @@ public override void Run(MemberMapper mapper, IList difference // So if in this version of left and right, right is unsealing the type, abstract members can be added. // checking for member additions on interfaces is checked on its own rule. var leftDeclaringType = mapper.DeclaringType.Left; - if (!leftDeclaringType.IsInterface && !leftDeclaringType.IsEffectivelySealed(/* TODO includeInternalSymbols */ false)) + if (leftDeclaringType is not null && !leftDeclaringType.IsInterface && !leftDeclaringType.IsEffectivelySealed(/* TODO includeInternalSymbols */ false)) { differences.Add(new CannotAddAbstractMemberDifference(mapper)); } diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotAddMemberToInterface.cs b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotAddMemberToInterface.cs similarity index 92% rename from src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotAddMemberToInterface.cs rename to src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotAddMemberToInterface.cs index df5a799..999f3f1 100644 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotAddMemberToInterface.cs +++ b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotAddMemberToInterface.cs @@ -16,7 +16,7 @@ public override void Run(MemberMapper mapper, IList difference { var (left, right) = mapper; - if (left == null && right != null && right.DeclaringType.IsInterface) + if (left == null && right is { DeclaringType: not null } && right.DeclaringType.IsInterface) { // Fields in interface can only be static which is not considered a break. if (right is FieldDefinition) @@ -36,7 +36,9 @@ public override void Run(MemberMapper mapper, IList difference } private static bool IsEventOrPropertyAccessor(MethodDefinition symbol) - { + { + if (symbol.DeclaringType is null) return false; + foreach (var property in symbol.DeclaringType.Properties) { if (symbol == property.GetMethod || symbol == property.SetMethod) diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotAddOrRemoveVirtualKeyword.cs b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotAddOrRemoveVirtualKeyword.cs similarity index 95% rename from src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotAddOrRemoveVirtualKeyword.cs rename to src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotAddOrRemoveVirtualKeyword.cs index 809f663..673a62b 100644 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotAddOrRemoveVirtualKeyword.cs +++ b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotAddOrRemoveVirtualKeyword.cs @@ -26,7 +26,7 @@ public override void Run(MemberMapper mapper, IList difference var (left, right) = mapper; // Members must exist - if (left is null || right is null) + if (left is not { DeclaringType: not null } || right is not { DeclaringType: not null }) { return; } diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotChangeGenericConstraints.cs b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotChangeGenericConstraints.cs similarity index 97% rename from src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotChangeGenericConstraints.cs rename to src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotChangeGenericConstraints.cs index 5273b6b..a5dc73e 100644 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotChangeGenericConstraints.cs +++ b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotChangeGenericConstraints.cs @@ -76,7 +76,7 @@ IList differences // for example: changing a constraint from MemoryStream to Stream on a sealed type, or non-virtual member // but we'll leave this to suppressions - addedConstraints.AddRange(rightOnlyConstraints.Select(x => x.FullName)); + addedConstraints.AddRange(rightOnlyConstraints.Where(x => x is not null).Select(x => x!.FullName)); // additions foreach (var addedConstraint in addedConstraints) @@ -93,7 +93,7 @@ IList differences { var leftOnlyConstraints = ToHashSet(leftTypeParam.Constraints); leftOnlyConstraints.ExceptWith(ToHashSet(rightTypeParam.Constraints)); - removedConstraints.AddRange(leftOnlyConstraints.Select(x => x.FullName)); + removedConstraints.AddRange(leftOnlyConstraints.Where(x => x is not null).Select(x => x!.FullName)); foreach (var removedConstraint in removedConstraints) { diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotChangeVisibility.cs b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotChangeVisibility.cs similarity index 89% rename from src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotChangeVisibility.cs rename to src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotChangeVisibility.cs index 11b2ddc..49debdf 100644 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotChangeVisibility.cs +++ b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotChangeVisibility.cs @@ -2,7 +2,7 @@ using CompatUnbreaker.Tool.ApiCompatibility.AssemblyMapping; using CompatUnbreaker.Tool.Utilities.AsmResolver; using CompatUnbreaker.Utilities.AsmResolver; -using Microsoft.CodeAnalysis; +//using Microsoft.CodeAnalysis; using Accessibility = CompatUnbreaker.Utilities.AsmResolver.Accessibility; namespace CompatUnbreaker.Tool.ApiCompatibility.Comparing.Rules; @@ -32,7 +32,7 @@ public override void Run(MemberMapper mapper, IList difference _ => a, }; - private int CompareAccessibility(Accessibility a, Accessibility b) + private static int CompareAccessibility(Accessibility a, Accessibility b) { // if (!_settings.IncludeInternalSymbols) TODO { @@ -59,7 +59,7 @@ private int CompareAccessibility(Accessibility a, Accessibility b) }; } - private void Run(IMemberDefinition? left, IMemberDefinition? right, IList differences) + private static void Run(IMemberDefinition? left, IMemberDefinition? right, IList differences) { // The MemberMustExist rule handles missing symbols and therefore this rule only runs when left and right is not null. if (left is null || right is null) @@ -69,7 +69,7 @@ private void Run(IMemberDefinition? left, IMemberDefinition? right, IList 0) { diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotRemoveBaseTypeOrInterface.cs b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotRemoveBaseTypeOrInterface.cs similarity index 95% rename from src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotRemoveBaseTypeOrInterface.cs rename to src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotRemoveBaseTypeOrInterface.cs index 143c3e9..acae5e8 100644 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/CannotRemoveBaseTypeOrInterface.cs +++ b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotRemoveBaseTypeOrInterface.cs @@ -7,7 +7,7 @@ namespace CompatUnbreaker.Tool.ApiCompatibility.Comparing.Rules; public sealed class CannotRemoveBaseTypeDifference(TypeMapper mapper) : TypeCompatDifference(mapper) { - public override string Message => $"Type '{Mapper.Left}' does not inherit from base type '{Mapper.Left.BaseType}' on {"right"} but it does on {"left"}"; + public override string Message => $"Type '{Mapper.Left}' does not inherit from base type '{Mapper.Left?.BaseType}' on {"right"} but it does on {"left"}"; public override DifferenceType Type => DifferenceType.Changed; } @@ -58,7 +58,7 @@ private void ValidateBaseTypeNotRemoved(TypeMapper mapper, IList Mapper.Right.IsSealed + public override string Message => Mapper.Right?.IsSealed ?? false ? $"Type '{Mapper.Right}' has the sealed modifier on {"right"} but not on {"left"}" : $"Type '{Mapper.Right}' is sealed because it has no visible constructor on {"right"} but it does on {"left"}"; diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/EnumsMustMatch.cs b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/EnumsMustMatch.cs similarity index 96% rename from src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/EnumsMustMatch.cs rename to src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/EnumsMustMatch.cs index 66bf20f..47fabb8 100644 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/EnumsMustMatch.cs +++ b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/EnumsMustMatch.cs @@ -6,7 +6,7 @@ namespace CompatUnbreaker.Tool.ApiCompatibility.Comparing.Rules; public sealed class EnumTypesMustMatch(TypeMapper mapper) : TypeCompatDifference(mapper) { - public override string Message => $"Underlying type of enum '{Mapper.Left}' changed from '{Mapper.Left.GetEnumUnderlyingType()}' to '{Mapper.Right.GetEnumUnderlyingType()}'"; + public override string Message => $"Underlying type of enum '{Mapper.Left}' changed from '{Mapper.Left?.GetEnumUnderlyingType()}' to '{Mapper.Right?.GetEnumUnderlyingType()}'"; public override DifferenceType Type => DifferenceType.Changed; } diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/MembersMustExist.cs b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/MembersMustExist.cs similarity index 100% rename from src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/Comparing/Rules/MembersMustExist.cs rename to src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/MembersMustExist.cs diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/README.md b/src/build/ArApiCompat/ApiCompatibility/README.md similarity index 100% rename from src/build/CompatUnbreaker/CompatUnbreaker.Tool/ApiCompatibility/README.md rename to src/build/ArApiCompat/ApiCompatibility/README.md diff --git a/src/build/ArApiCompat/ArApiCompat.csproj b/src/build/ArApiCompat/ArApiCompat.csproj new file mode 100644 index 0000000..08d27ea --- /dev/null +++ b/src/build/ArApiCompat/ArApiCompat.csproj @@ -0,0 +1,13 @@ + + + + net9.0 + enable + enable + + + + + + + diff --git a/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/Accessibility.cs b/src/build/ArApiCompat/Utilities/AsmResolver/Accessibility.cs similarity index 100% rename from src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/Accessibility.cs rename to src/build/ArApiCompat/Utilities/AsmResolver/Accessibility.cs diff --git a/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/AccessibilityExtensions.cs b/src/build/ArApiCompat/Utilities/AsmResolver/AccessibilityExtensions.cs similarity index 100% rename from src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/AccessibilityExtensions.cs rename to src/build/ArApiCompat/Utilities/AsmResolver/AccessibilityExtensions.cs diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/Utilities/AsmResolver/DefinitionModifiersExtensions.cs b/src/build/ArApiCompat/Utilities/AsmResolver/DefinitionModifiersExtensions.cs similarity index 100% rename from src/build/CompatUnbreaker/CompatUnbreaker.Tool/Utilities/AsmResolver/DefinitionModifiersExtensions.cs rename to src/build/ArApiCompat/Utilities/AsmResolver/DefinitionModifiersExtensions.cs diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/Utilities/AsmResolver/ExtendedSignatureComparer.cs b/src/build/ArApiCompat/Utilities/AsmResolver/ExtendedSignatureComparer.cs similarity index 100% rename from src/build/CompatUnbreaker/CompatUnbreaker.Tool/Utilities/AsmResolver/ExtendedSignatureComparer.cs rename to src/build/ArApiCompat/Utilities/AsmResolver/ExtendedSignatureComparer.cs diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/Utilities/AsmResolver/GenericParameterExtensions.cs b/src/build/ArApiCompat/Utilities/AsmResolver/GenericParameterExtensions.cs similarity index 100% rename from src/build/CompatUnbreaker/CompatUnbreaker.Tool/Utilities/AsmResolver/GenericParameterExtensions.cs rename to src/build/ArApiCompat/Utilities/AsmResolver/GenericParameterExtensions.cs diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/Utilities/AsmResolver/MemberDefinitionExtensions.cs b/src/build/ArApiCompat/Utilities/AsmResolver/MemberDefinitionExtensions.cs similarity index 87% rename from src/build/CompatUnbreaker/CompatUnbreaker.Tool/Utilities/AsmResolver/MemberDefinitionExtensions.cs rename to src/build/ArApiCompat/Utilities/AsmResolver/MemberDefinitionExtensions.cs index 24655b2..95172ee 100644 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/Utilities/AsmResolver/MemberDefinitionExtensions.cs +++ b/src/build/ArApiCompat/Utilities/AsmResolver/MemberDefinitionExtensions.cs @@ -4,6 +4,18 @@ namespace CompatUnbreaker.Tool.Utilities.AsmResolver; internal static class MemberDefinitionExtensions { + public static bool IsStatic(this IMemberDefinition member) + { + return member switch + { + TypeDefinition type => type is { IsSealed: true, IsAbstract: true }, + MethodDefinition method => method.IsStatic, + FieldDefinition field => field.IsStatic, + PropertyDefinition property => property.GetMethod?.IsStatic != false && property.SetMethod?.IsStatic != false, + EventDefinition @event => @event.AddMethod?.IsStatic != false && @event.RemoveMethod?.IsStatic != false, + _ => throw new ArgumentOutOfRangeException(nameof(member)), + }; + } public static bool IsRoslynAbstract(this IMemberDefinition member) { return member switch diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/Utilities/AsmResolver/MethodDefinitionExtensions.cs b/src/build/ArApiCompat/Utilities/AsmResolver/MethodDefinitionExtensions.cs similarity index 100% rename from src/build/CompatUnbreaker/CompatUnbreaker.Tool/Utilities/AsmResolver/MethodDefinitionExtensions.cs rename to src/build/ArApiCompat/Utilities/AsmResolver/MethodDefinitionExtensions.cs diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/Utilities/AsmResolver/MiscellaneousExtensions.cs b/src/build/ArApiCompat/Utilities/AsmResolver/MiscellaneousExtensions.cs similarity index 100% rename from src/build/CompatUnbreaker/CompatUnbreaker.Tool/Utilities/AsmResolver/MiscellaneousExtensions.cs rename to src/build/ArApiCompat/Utilities/AsmResolver/MiscellaneousExtensions.cs diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/Utilities/AsmResolver/TypeDefinitionExtensions.InterfaceMapping.cs b/src/build/ArApiCompat/Utilities/AsmResolver/TypeDefinitionExtensions.InterfaceMapping.cs similarity index 100% rename from src/build/CompatUnbreaker/CompatUnbreaker.Tool/Utilities/AsmResolver/TypeDefinitionExtensions.InterfaceMapping.cs rename to src/build/ArApiCompat/Utilities/AsmResolver/TypeDefinitionExtensions.InterfaceMapping.cs diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/Utilities/AsmResolver/TypeDefinitionExtensions.cs b/src/build/ArApiCompat/Utilities/AsmResolver/TypeDefinitionExtensions.cs similarity index 100% rename from src/build/CompatUnbreaker/CompatUnbreaker.Tool/Utilities/AsmResolver/TypeDefinitionExtensions.cs rename to src/build/ArApiCompat/Utilities/AsmResolver/TypeDefinitionExtensions.cs diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/Utilities/AsmResolver/TypeSignatureExtensions.cs b/src/build/ArApiCompat/Utilities/AsmResolver/TypeSignatureExtensions.cs similarity index 100% rename from src/build/CompatUnbreaker/CompatUnbreaker.Tool/Utilities/AsmResolver/TypeSignatureExtensions.cs rename to src/build/ArApiCompat/Utilities/AsmResolver/TypeSignatureExtensions.cs diff --git a/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/VisibilityExtensions.cs b/src/build/ArApiCompat/Utilities/AsmResolver/VisibilityExtensions.cs similarity index 100% rename from src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/VisibilityExtensions.cs rename to src/build/ArApiCompat/Utilities/AsmResolver/VisibilityExtensions.cs diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/Utilities/MetadataHelpers.cs b/src/build/ArApiCompat/Utilities/MetadataHelpers.cs similarity index 100% rename from src/build/CompatUnbreaker/CompatUnbreaker.Tool/Utilities/MetadataHelpers.cs rename to src/build/ArApiCompat/Utilities/MetadataHelpers.cs diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/Utilities/StringExtensions.cs b/src/build/ArApiCompat/Utilities/StringExtensions.cs similarity index 100% rename from src/build/CompatUnbreaker/CompatUnbreaker.Tool/Utilities/StringExtensions.cs rename to src/build/ArApiCompat/Utilities/StringExtensions.cs diff --git a/src/build/CompatUnbreaker/.github/workflows/main.yml b/src/build/CompatUnbreaker/.github/workflows/main.yml deleted file mode 100644 index e3146e2..0000000 --- a/src/build/CompatUnbreaker/.github/workflows/main.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: CI - -on: [ "push", "pull_request" ] - -env: - DOTNET_CLI_TELEMETRY_OPTOUT: true - DOTNET_NOLOGO: true - - # Make all dotnet builds default to Release configuration - Configuration: Release - -jobs: - build: - runs-on: ubuntu-22.04 - - env: - NUGET_ARTIFACTS: artifacts/package/release/*.nupkg - - steps: - - uses: actions/checkout@v5 - with: - fetch-depth: 0 - filter: tree:0 - - - name: Setup .NET - uses: js6pak/setup-dotnet@8a21b4ed1527c384b6d4b7919db11dcb8389065c # https://github.com/actions/setup-dotnet/pull/538 - with: - global-json-file: global.json - - - name: Restore - run: dotnet restore --locked-mode - - - name: Build - run: dotnet build --no-restore - - - uses: actions/upload-artifact@v4 - with: - name: nuget - path: ${{ env.NUGET_ARTIFACTS }} - - - name: Format - run: dotnet format --no-restore --verbosity diagnostic --verify-no-changes - - - name: Test - run: dotnet test --no-restore --logger GitHubActions - - - name: Publish nightly - if: vars.NIGHTLY_NUGET_SOURCE != '' && (github.ref == 'refs/heads/master' || github.ref_type == 'tag') - env: - NUGET_SOURCE: ${{ vars.NIGHTLY_NUGET_SOURCE }} - NUGET_API_KEY: ${{ secrets.NIGHTLY_NUGET_API_KEY }} - run: | - dotnet nuget push --skip-duplicate $NUGET_ARTIFACTS \ - --source "$NUGET_SOURCE" --api-key "$NUGET_API_KEY" - - - name: Publish - if: github.ref_type == 'tag' - env: - NUGET_SOURCE: ${{ vars.NUGET_SOURCE || 'nuget.org' }} - NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} - run: | - dotnet nuget push --skip-duplicate $NUGET_ARTIFACTS \ - --source "$NUGET_SOURCE" --api-key "$NUGET_API_KEY" diff --git a/src/build/CompatUnbreaker/.gitignore b/src/build/CompatUnbreaker/.gitignore deleted file mode 100644 index 7b83424..0000000 --- a/src/build/CompatUnbreaker/.gitignore +++ /dev/null @@ -1,23 +0,0 @@ -# Build results -bin/ -obj/ -artifacts/ - -# User-specific files -*.user -.env - -# MSBuild Binary and Structured Log -*.binlog - -# JetBrains Rider -.idea/ - -# VS Code -.vscode/ - -# Visual Studio -.vs/ - -# .editorconfig is generated by OpinionPak -/.editorconfig diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Attributes/CompatUnbreaker.Attributes.csproj b/src/build/CompatUnbreaker/CompatUnbreaker.Attributes/CompatUnbreaker.Attributes.csproj deleted file mode 100644 index 9f5a0cf..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Attributes/CompatUnbreaker.Attributes.csproj +++ /dev/null @@ -1,11 +0,0 @@ - - - true - net9.0;netstandard2.0;net35 - - - - - - - diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Attributes/NotSupportedAnymoreException.cs b/src/build/CompatUnbreaker/CompatUnbreaker.Attributes/NotSupportedAnymoreException.cs deleted file mode 100644 index 5e9970c..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Attributes/NotSupportedAnymoreException.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace CompatUnbreaker.Attributes; - -public sealed class NotSupportedAnymoreException : NotSupportedException -{ - public NotSupportedAnymoreException() - { - } - - public NotSupportedAnymoreException(string message) : base(message) - { - } - - public NotSupportedAnymoreException(string message, Exception innerException) : base(message, innerException) - { - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Attributes/UnbreakerConstructorAttribute.cs b/src/build/CompatUnbreaker/CompatUnbreaker.Attributes/UnbreakerConstructorAttribute.cs deleted file mode 100644 index 73b4f6b..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Attributes/UnbreakerConstructorAttribute.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace CompatUnbreaker.Attributes; - -[AttributeUsage(AttributeTargets.Method)] -public sealed class UnbreakerConstructorAttribute : Attribute; diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Attributes/UnbreakerExtensionAttribute.cs b/src/build/CompatUnbreaker/CompatUnbreaker.Attributes/UnbreakerExtensionAttribute.cs deleted file mode 100644 index 459e6d3..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Attributes/UnbreakerExtensionAttribute.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace CompatUnbreaker.Attributes; - -[AttributeUsage(AttributeTargets.Class)] -public sealed class UnbreakerExtensionAttribute : Attribute -{ - public UnbreakerExtensionAttribute(Type type) - { - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Attributes/UnbreakerExtensionsAttribute.cs b/src/build/CompatUnbreaker/CompatUnbreaker.Attributes/UnbreakerExtensionsAttribute.cs deleted file mode 100644 index f6e0113..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Attributes/UnbreakerExtensionsAttribute.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace CompatUnbreaker.Attributes; - -[AttributeUsage(AttributeTargets.Class)] -public sealed class UnbreakerExtensionsAttribute : Attribute; diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Attributes/UnbreakerFieldAttribute.cs b/src/build/CompatUnbreaker/CompatUnbreaker.Attributes/UnbreakerFieldAttribute.cs deleted file mode 100644 index e9564ca..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Attributes/UnbreakerFieldAttribute.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace CompatUnbreaker.Attributes; - -[AttributeUsage(AttributeTargets.Property)] -public sealed class UnbreakerFieldAttribute : Attribute; diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Attributes/UnbreakerRenameAttribute.cs b/src/build/CompatUnbreaker/CompatUnbreaker.Attributes/UnbreakerRenameAttribute.cs deleted file mode 100644 index bfe19eb..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Attributes/UnbreakerRenameAttribute.cs +++ /dev/null @@ -1,45 +0,0 @@ -namespace CompatUnbreaker.Attributes; - -[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] -public sealed class UnbreakerRenameAttribute : Attribute -{ - public UnbreakerRenameAttribute(string namespaceName, string newNamespaceName) - { - } - - public UnbreakerRenameAttribute(Type type, string newTypeName) - { - } - - public UnbreakerRenameAttribute(Type type, string memberName, string newMemberName) - { - } -} - -// TODO maybe switch to this? -public static class UnbreakerRename -{ - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - public sealed class NamespaceAttribute : Attribute - { - public NamespaceAttribute(string name, string newName) - { - } - } - - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - public sealed class TypeAttribute : Attribute - { - public TypeAttribute(Type type, string newName) - { - } - } - - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - public sealed class TypeMemberAttribute : Attribute - { - public TypeMemberAttribute(Type type, string memberName, string newMemberName) - { - } - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Attributes/UnbreakerReplaceAttribute.cs b/src/build/CompatUnbreaker/CompatUnbreaker.Attributes/UnbreakerReplaceAttribute.cs deleted file mode 100644 index 2a3a9a3..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Attributes/UnbreakerReplaceAttribute.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace CompatUnbreaker.Attributes; - -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] -public sealed class UnbreakerReplaceAttribute : Attribute -{ - public UnbreakerReplaceAttribute(Type type) - { - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Attributes/UnbreakerShimAttribute.cs b/src/build/CompatUnbreaker/CompatUnbreaker.Attributes/UnbreakerShimAttribute.cs deleted file mode 100644 index 4430511..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Attributes/UnbreakerShimAttribute.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace CompatUnbreaker.Attributes; - -[AttributeUsage(AttributeTargets.Assembly)] -public sealed class UnbreakerShimAttribute : Attribute -{ - public UnbreakerShimAttribute(string targetAssemblyName) - { - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Attributes/UnbreakerThisAttribute.cs b/src/build/CompatUnbreaker/CompatUnbreaker.Attributes/UnbreakerThisAttribute.cs deleted file mode 100644 index 119e150..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Attributes/UnbreakerThisAttribute.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace CompatUnbreaker.Attributes; - -/// -/// A substitute for the keyword, to allow shimming extension methods. -/// -[AttributeUsage(AttributeTargets.Parameter)] -public sealed class UnbreakerThisAttribute : Attribute; diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Attributes/packages.lock.json b/src/build/CompatUnbreaker/CompatUnbreaker.Attributes/packages.lock.json deleted file mode 100644 index 29e7a20..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Attributes/packages.lock.json +++ /dev/null @@ -1,117 +0,0 @@ -{ - "version": 1, - "dependencies": { - ".NETFramework,Version=v3.5": { - "Meziantou.Analyzer": { - "type": "Direct", - "requested": "[2.0.220, )", - "resolved": "2.0.220", - "contentHash": "LknH5tduuxY6jzPGUIBHrszk1fepyEWAD9Ht7Hq1rrGZ02BKx54ADXzrlXTkLNpnkVzRBHU4bO6HH7GwIeDR+w==" - }, - "Microsoft.NETFramework.ReferenceAssemblies": { - "type": "Direct", - "requested": "[1.0.3, )", - "resolved": "1.0.3", - "contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==", - "dependencies": { - "Microsoft.NETFramework.ReferenceAssemblies.net35": "1.0.3" - } - }, - "PolySharp": { - "type": "Direct", - "requested": "[1.15.0, )", - "resolved": "1.15.0", - "contentHash": "FbU0El+EEjdpuIX4iDbeS7ki1uzpJPx8vbqOzEtqnl1GZeAGJfq+jCbxeJL2y0EPnUNk8dRnnqR2xnYXg9Tf+g==" - }, - "StyleCop.Analyzers": { - "type": "Direct", - "requested": "[1.2.0-beta.556, )", - "resolved": "1.2.0-beta.556", - "contentHash": "llRPgmA1fhC0I0QyFLEcjvtM2239QzKr/tcnbsjArLMJxJlu0AA5G7Fft0OI30pHF3MW63Gf4aSSsjc5m82J1Q==", - "dependencies": { - "StyleCop.Analyzers.Unstable": "1.2.0.556" - } - }, - "Microsoft.NETFramework.ReferenceAssemblies.net35": { - "type": "Transitive", - "resolved": "1.0.3", - "contentHash": "Mhbr13IGgsW4AHj50uAiVNMPWdvUPWePRsI4x1NiTkhHUc4rqi9XvY5xxBC4d56bJypQbxAZ8bPuZIpz+TjBVQ==" - }, - "StyleCop.Analyzers.Unstable": { - "type": "Transitive", - "resolved": "1.2.0.556", - "contentHash": "zvn9Mqs/ox/83cpYPignI8hJEM2A93s2HkHs8HYMOAQW0PkampyoErAiIyKxgTLqbbad29HX/shv/6LGSjPJNQ==" - } - }, - ".NETStandard,Version=v2.0": { - "Meziantou.Analyzer": { - "type": "Direct", - "requested": "[2.0.220, )", - "resolved": "2.0.220", - "contentHash": "LknH5tduuxY6jzPGUIBHrszk1fepyEWAD9Ht7Hq1rrGZ02BKx54ADXzrlXTkLNpnkVzRBHU4bO6HH7GwIeDR+w==" - }, - "NETStandard.Library": { - "type": "Direct", - "requested": "[2.0.3, )", - "resolved": "2.0.3", - "contentHash": "st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0" - } - }, - "PolySharp": { - "type": "Direct", - "requested": "[1.15.0, )", - "resolved": "1.15.0", - "contentHash": "FbU0El+EEjdpuIX4iDbeS7ki1uzpJPx8vbqOzEtqnl1GZeAGJfq+jCbxeJL2y0EPnUNk8dRnnqR2xnYXg9Tf+g==" - }, - "StyleCop.Analyzers": { - "type": "Direct", - "requested": "[1.2.0-beta.556, )", - "resolved": "1.2.0-beta.556", - "contentHash": "llRPgmA1fhC0I0QyFLEcjvtM2239QzKr/tcnbsjArLMJxJlu0AA5G7Fft0OI30pHF3MW63Gf4aSSsjc5m82J1Q==", - "dependencies": { - "StyleCop.Analyzers.Unstable": "1.2.0.556" - } - }, - "Microsoft.NETCore.Platforms": { - "type": "Transitive", - "resolved": "1.1.0", - "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" - }, - "StyleCop.Analyzers.Unstable": { - "type": "Transitive", - "resolved": "1.2.0.556", - "contentHash": "zvn9Mqs/ox/83cpYPignI8hJEM2A93s2HkHs8HYMOAQW0PkampyoErAiIyKxgTLqbbad29HX/shv/6LGSjPJNQ==" - } - }, - "net9.0": { - "Meziantou.Analyzer": { - "type": "Direct", - "requested": "[2.0.220, )", - "resolved": "2.0.220", - "contentHash": "LknH5tduuxY6jzPGUIBHrszk1fepyEWAD9Ht7Hq1rrGZ02BKx54ADXzrlXTkLNpnkVzRBHU4bO6HH7GwIeDR+w==" - }, - "PolySharp": { - "type": "Direct", - "requested": "[1.15.0, )", - "resolved": "1.15.0", - "contentHash": "FbU0El+EEjdpuIX4iDbeS7ki1uzpJPx8vbqOzEtqnl1GZeAGJfq+jCbxeJL2y0EPnUNk8dRnnqR2xnYXg9Tf+g==" - }, - "StyleCop.Analyzers": { - "type": "Direct", - "requested": "[1.2.0-beta.556, )", - "resolved": "1.2.0-beta.556", - "contentHash": "llRPgmA1fhC0I0QyFLEcjvtM2239QzKr/tcnbsjArLMJxJlu0AA5G7Fft0OI30pHF3MW63Gf4aSSsjc5m82J1Q==", - "dependencies": { - "StyleCop.Analyzers.Unstable": "1.2.0.556" - } - }, - "StyleCop.Analyzers.Unstable": { - "type": "Transitive", - "resolved": "1.2.0.556", - "contentHash": "zvn9Mqs/ox/83cpYPignI8hJEM2A93s2HkHs8HYMOAQW0PkampyoErAiIyKxgTLqbbad29HX/shv/6LGSjPJNQ==" - } - } - } -} \ No newline at end of file diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tests/AsmResolverSyntaxGeneratorTests.cs b/src/build/CompatUnbreaker/CompatUnbreaker.Tests/AsmResolverSyntaxGeneratorTests.cs deleted file mode 100644 index fe82367..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Tests/AsmResolverSyntaxGeneratorTests.cs +++ /dev/null @@ -1,172 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using AsmResolver.DotNet; -using CompatUnbreaker.Tool.SkeletonGeneration.CodeGeneration; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Editing; -using Microsoft.CodeAnalysis.Formatting; -using Microsoft.CodeAnalysis.Simplification; -using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; -using Assembly = System.Reflection.Assembly; - -namespace CompatUnbreaker.Tests; - -public sealed class AsmResolverSyntaxGeneratorTests -{ - public static IEnumerable<(string FileName, string Source)> DataSource() - { - var assembly = Assembly.GetExecutingAssembly(); - var names = assembly.GetManifestResourceNames(); - - const string Prefix = "CompatUnbreaker.Tests.TestFiles."; - - foreach (var name in names) - { - if (!name.StartsWith(Prefix, StringComparison.Ordinal)) continue; - - var fileName = name[Prefix.Length..]; - using var streamReader = new StreamReader(assembly.GetManifestResourceStream(name)!); - var source = streamReader.ReadToEnd(); - - yield return (fileName, source); - } - } - - [Test] - [MethodDataSource(nameof(DataSource))] - [DisplayName("Roundtrip($fileName)")] - public async Task Roundtrip(string fileName, string source) - { - var bytes = Compile(source, out var sourceSyntaxTree); - - var assemblyDefinition = AssemblyDefinition.FromBytes(bytes); - - using var workspace = new AdhocWorkspace(); - var project = workspace.AddProject("Test", LanguageNames.CSharp) - .WithMetadataReferences(Basic.Reference.Assemblies.Net90.References.All) - .WithCompilationOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary) - .WithOptimizationLevel(OptimizationLevel.Release) - .WithAllowUnsafe(true) - .WithNullableContextOptions(NullableContextOptions.Enable)); - - project = project.AddDocument("GlobalUsings.g.cs", GlobalUsingsText).Project; - - var addedDocuments = new List(); - - foreach (var module in assemblyDefinition.Modules) - { - foreach (var type in module.TopLevelTypes) - { - if (type.IsModuleType || type.IsCompilerGenerated()) continue; - - var syntaxGenerator = SyntaxGenerator.GetGenerator(project); - var member = (MemberDeclarationSyntax) syntaxGenerator.Declaration(type); - - if (type.Namespace is { } @namespace) - { - member = FileScopedNamespaceDeclaration((NameSyntax) syntaxGenerator.DottedName(@namespace)) - .AddMembers(member); - } - - var syntaxRoot = CompilationUnit() - .AddMembers(member) - .WithTrailingTrivia(LineFeed) - .WithAdditionalAnnotations(Simplifier.Annotation, Simplifier.AddImportsAnnotation); - - var document = project.AddDocument("TestClass", syntaxRoot); - addedDocuments.Add(document.Id); - project = document.Project; - } - } - - foreach (var documentId in addedDocuments) - { - var document = project.GetDocument(documentId); - - document = await ImportAdder.AddImportsAsync(document); - document = await Simplifier.ReduceAsync(document); - document = await Formatter.FormatAsync(document); - - var text = (await document.GetTextAsync()).ToString(); - Console.WriteLine(text); - - var syntaxTree = await document.GetSyntaxTreeAsync(); - ArgumentNullException.ThrowIfNull(syntaxTree); - - if (!sourceSyntaxTree.IsEquivalentTo(syntaxTree)) - { - await Assert.That(ToStringWithoutTrivia(syntaxTree)).IsEqualTo(ToStringWithoutTrivia(sourceSyntaxTree)); - } - - project = document.Project; - } - } - - private static string ToStringWithoutTrivia(SyntaxTree tree) - { - var syntaxNode = tree.GetRoot(); - syntaxNode = TriviaRemover.Instance.Visit(syntaxNode); - return syntaxNode.NormalizeWhitespace().ToFullString(); - } - - private sealed class TriviaRemover : CSharpSyntaxRewriter - { - public static TriviaRemover Instance { get; } = new(); - - [return: NotNullIfNotNull(nameof(node))] - public override SyntaxNode? Visit(SyntaxNode? node) - { - return base.Visit(node)?.WithoutTrivia(); - } - - public override SyntaxTrivia VisitTrivia(SyntaxTrivia trivia) - { - return SyntaxFactory.Whitespace(""); - } - } - - [StringSyntax(LanguageNames.CSharp)] - private const string GlobalUsingsText = - """ - // - global using System; - global using System.Collections.Generic; - global using System.IO; - global using System.Linq; - global using System.Net.Http; - global using System.Threading; - global using System.Threading.Tasks; - """; - - private static byte[] Compile([StringSyntax(LanguageNames.CSharp)] string source, out SyntaxTree syntaxTree) - { - var compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary) - .WithOptimizationLevel(OptimizationLevel.Release) - .WithAllowUnsafe(true) - .WithNullableContextOptions(NullableContextOptions.Enable); - - var references = Basic.Reference.Assemblies.Net90.References.All; - - syntaxTree = ParseSyntaxTree(source); - - var globalUsings = ParseSyntaxTree(GlobalUsingsText); - - var compilation = CSharpCompilation.Create("Test", [globalUsings, syntaxTree], references, compilationOptions); - - var stream = new MemoryStream(); - - var emitResult = compilation.Emit(stream); - if (!emitResult.Success) - { - foreach (var diagnostic in emitResult.Diagnostics) - { - Console.WriteLine(diagnostic); - } - - throw new Exception(); - } - - return stream.GetBuffer(); - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tests/CompatUnbreaker.Tests.csproj b/src/build/CompatUnbreaker/CompatUnbreaker.Tests/CompatUnbreaker.Tests.csproj deleted file mode 100644 index 583d837..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Tests/CompatUnbreaker.Tests.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - net$(NETCoreAppMaximumVersion) - Exe - true - - - - - - - - - - - - - - - - diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tests/TestFiles/AttributeTests.cs b/src/build/CompatUnbreaker/CompatUnbreaker.Tests/TestFiles/AttributeTests.cs deleted file mode 100644 index 544480a..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Tests/TestFiles/AttributeTests.cs +++ /dev/null @@ -1,80 +0,0 @@ -namespace CompatUnbreaker.Tests.TestFiles; - -[Test("string")] -[Test(new string[] { "a", "b" })] -[Test(123)] -[Test(typeof(AttributeTests))] -[Test(typeof(Action<,>))] -// [Test(TestEnum.B)] TODO this requires porting CSharpFlagsEnumGenerator -[Test((object?) null)] -[Test((string?) null)] -[Test((string[]?) null)] -[Test((Type?) null)] -public sealed class AttributeTests -{ - public const AttributeTargets Guh = AttributeTargets.Assembly | AttributeTargets.Interface | AttributeTargets.Delegate; - - [Test] - public int field; - - [Test] - public extern int Property { get; set; } - - [Test] - public extern event Action Event; - - [Test] - public AttributeTests() - { - } - - [Test] - [return: Test] - public static extern T Method<[Test] T>([Test] T parameter); - - [Test] - public delegate T Delegate(); - - // [Test] - // public interface IInterface - // { - // } - - [Test] - public enum TestEnum : byte - { - A = 1, - B = 2 - } - - [Test] - public struct TestStruct; - - [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] - public sealed class TestAttribute : Attribute - { - public TestAttribute() - { - } - - public TestAttribute(string? a) - { - } - - public TestAttribute(string[]? a) - { - } - - public TestAttribute(object? a) - { - } - - public TestAttribute(Type? a) - { - } - - public TestAttribute(TestEnum a) - { - } - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tests/TestFiles/DllImportTests.cs b/src/build/CompatUnbreaker/CompatUnbreaker.Tests/TestFiles/DllImportTests.cs deleted file mode 100644 index bad0e7f..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Tests/TestFiles/DllImportTests.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Runtime.InteropServices; - -namespace CompatUnbreaker.Tests.TestFiles; - -public static class DllImportTests -{ - [DllImport("cat")] - public static extern void Meow(); - - [DllImport("cat", EntryPoint = "meow", CharSet = CharSet.Ansi, SetLastError = true, ExactSpelling = true, CallingConvention = CallingConvention.FastCall, BestFitMapping = false, PreserveSig = false, ThrowOnUnmappableChar = false)] - public static extern void MeowFull(); -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tests/TestFiles/MethodTests.cs b/src/build/CompatUnbreaker/CompatUnbreaker.Tests/TestFiles/MethodTests.cs deleted file mode 100644 index f9c2556..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Tests/TestFiles/MethodTests.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -namespace CompatUnbreaker.Tests.TestFiles; - -public sealed class MethodTests -{ - public static extern void Method(); - - public static extern ref readonly int AllTheRefs(ref int @ref, ref readonly int refReadonly, in int @in, out int @out, scoped ref int scopedRef, [UnscopedRef] ref int unscopedRef, scoped Span scopedSpan); - - public static extern void ScopedGeneric(scoped T a) where T : unmanaged, allows ref struct; - - public static extern T Generic(T t) where T : class, IDisposable, new(); - - public static extern void Array(int[] a, int[][] b, int[,] c); - - public static extern void Params(params int[] a); - - public static extern void ParamsSpan(params ReadOnlySpan a); - - public static extern void Default(int a = 123); - - // TODO needs RequiresUnsafeModifier implemented - // public static extern unsafe void Pointer(int* a); - // public static extern unsafe void FunctionPointer(delegate* a, delegate* unmanaged[Stdcall, SuppressGCTransition] b); -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tests/TestFiles/NamedTupleTests.cs b/src/build/CompatUnbreaker/CompatUnbreaker.Tests/TestFiles/NamedTupleTests.cs deleted file mode 100644 index deeff73..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Tests/TestFiles/NamedTupleTests.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace CompatUnbreaker.Tests.TestFiles; - -public sealed class NamedTupleTests -{ - public - ( - (object? aa1, object aa2) a1, object a2, object a3, object a4, object a5, object a6, object a7, object a8, - object b1, (object ba1, object ba2) b2, object b3, object b4, object? b5, object b6, object b7, object b8, - object c1, object c2, (object ca1, object ca2) c3, object c4, object c5, object c6, object c7, object c8, - object d1, object d2, object d3, (object da1, object? da2) d4, object d5, object d6, object d7, object d8 - ) longgg; - - public (object, object) unnamed; -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tests/TestFiles/ReservedTests.cs b/src/build/CompatUnbreaker/CompatUnbreaker.Tests/TestFiles/ReservedTests.cs deleted file mode 100644 index 08e4d5c..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Tests/TestFiles/ReservedTests.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace CompatUnbreaker.Tests.TestFiles; - -public static class ReservedTests -{ - public static extern dynamic Dynamic(dynamic a, dynamic[] b, (dynamic, object, int, dynamic) c); - - public static extern ref readonly int ReadOnly(); - - public static extern void RequiredLocation(ref readonly int a); - - public static extern void IsUnmanaged() where T : unmanaged; - - public static extern (int A, ((int E, int F) C, int D) B) TupleElementNames(); - - public static extern object? Nullable(object? a, object b, object? c, object d, T? e, object?[][]? f); - - public static extern void Extension(this object o); - - public ref struct IsByRefLike; - - public class Generics - where A : notnull - where B : class - where C : class? - { - public required A a; - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tests/packages.lock.json b/src/build/CompatUnbreaker/CompatUnbreaker.Tests/packages.lock.json deleted file mode 100644 index 6f3e4c5..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Tests/packages.lock.json +++ /dev/null @@ -1,567 +0,0 @@ -{ - "version": 1, - "dependencies": { - "net9.0": { - "Meziantou.Analyzer": { - "type": "Direct", - "requested": "[2.0.220, )", - "resolved": "2.0.220", - "contentHash": "LknH5tduuxY6jzPGUIBHrszk1fepyEWAD9Ht7Hq1rrGZ02BKx54ADXzrlXTkLNpnkVzRBHU4bO6HH7GwIeDR+w==" - }, - "PolySharp": { - "type": "Direct", - "requested": "[1.15.0, )", - "resolved": "1.15.0", - "contentHash": "FbU0El+EEjdpuIX4iDbeS7ki1uzpJPx8vbqOzEtqnl1GZeAGJfq+jCbxeJL2y0EPnUNk8dRnnqR2xnYXg9Tf+g==" - }, - "StyleCop.Analyzers": { - "type": "Direct", - "requested": "[1.2.0-beta.556, )", - "resolved": "1.2.0-beta.556", - "contentHash": "llRPgmA1fhC0I0QyFLEcjvtM2239QzKr/tcnbsjArLMJxJlu0AA5G7Fft0OI30pHF3MW63Gf4aSSsjc5m82J1Q==", - "dependencies": { - "StyleCop.Analyzers.Unstable": "1.2.0.556" - } - }, - "TUnit": { - "type": "Direct", - "requested": "[0.75.30, )", - "resolved": "0.75.30", - "contentHash": "ms5ssGbyPCA2cgqjpjXDH51EjuIkVtdiI0mH5v95bfn0lYVCaPD9CWP213Gm8tUkpE3enHIEGH5M2iQ85IyHIQ==", - "dependencies": { - "TUnit.Assertions": "0.75.30", - "TUnit.Engine": "0.75.30" - } - }, - "AsmResolver": { - "type": "Transitive", - "resolved": "6.0.0-dev" - }, - "AsmResolver.DotNet": { - "type": "Transitive", - "resolved": "6.0.0-dev", - "dependencies": { - "AsmResolver.PE": "6.0.0-dev" - } - }, - "AsmResolver.PE": { - "type": "Transitive", - "resolved": "6.0.0-dev", - "dependencies": { - "AsmResolver.PE.File": "6.0.0-dev" - } - }, - "AsmResolver.PE.File": { - "type": "Transitive", - "resolved": "6.0.0-dev", - "dependencies": { - "AsmResolver": "6.0.0-dev" - } - }, - "Basic.Reference.Assemblies.Net90": { - "type": "Transitive", - "resolved": "1.8.3", - "contentHash": "sZRH0NW/jdLTNq8X2/IVGIUVVE0JLKxwSp9GOwKzoHzt7BK5U7iRqG25ddSsggdABpvyz3ooQ4GkwQQ/Kn+z+g==", - "dependencies": { - "Microsoft.CodeAnalysis.Common": "4.11.0" - } - }, - "EnumerableAsyncProcessor": { - "type": "Transitive", - "resolved": "3.8.4", - "contentHash": "KlbpupRCz3Kf+P7gsiDvFXJ980i/9lfihMZFmmxIk0Gf6mopEjy74OTJZmdaKDQpE29eQDBnMZB5khyW3eugrg==" - }, - "Humanizer.Core": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==" - }, - "Microsoft.Build": { - "type": "Transitive", - "resolved": "17.14.28", - "contentHash": "MmGLEsROW1C9dH/d4sOqUX0sVNs2uwTCFXRQb89+pYNWDNJE+7bTJG9kOCbHeCH252XLnP55KIaOgwSpf6J4Kw==", - "dependencies": { - "Microsoft.Build.Framework": "17.14.28", - "Microsoft.NET.StringTools": "17.14.28", - "System.Configuration.ConfigurationManager": "9.0.0", - "System.Diagnostics.EventLog": "9.0.0", - "System.Reflection.MetadataLoadContext": "9.0.0", - "System.Security.Cryptography.ProtectedData": "9.0.0" - } - }, - "Microsoft.Build.Framework": { - "type": "Transitive", - "resolved": "17.14.28", - "contentHash": "wRcyTzGV0LRAtFdrddtioh59Ky4/zbvyraP0cQkDzRSRkhgAQb0K88D/JNC6VHLIXanRi3mtV1jU0uQkBwmiVg==" - }, - "Microsoft.Build.Locator": { - "type": "Transitive", - "resolved": "1.10.2", - "contentHash": "F+nLS7IpgtslyxNvtD6Jalnf5WU08lu8yfJBNQl3cbEF3AMUphs4t7nPuRYaaU8QZyGrqtVi7i73LhAe/yHx7A==" - }, - "Microsoft.Build.Tasks.Core": { - "type": "Transitive", - "resolved": "17.14.28", - "contentHash": "jk3O0tXp9QWPXhLJ7Pl8wm/eGtGgA1++vwHGWEmnwMU6eP//ghtcCUpQh9CQMwEKGDnH0aJf285V1s8yiSlKfQ==", - "dependencies": { - "Microsoft.Build.Framework": "17.14.28", - "Microsoft.Build.Utilities.Core": "17.14.28", - "Microsoft.NET.StringTools": "17.14.28", - "System.CodeDom": "9.0.0", - "System.Collections.Immutable": "9.0.0", - "System.Configuration.ConfigurationManager": "9.0.0", - "System.Diagnostics.EventLog": "9.0.0", - "System.Formats.Nrbf": "9.0.0", - "System.Resources.Extensions": "9.0.0", - "System.Security.Cryptography.Pkcs": "9.0.0", - "System.Security.Cryptography.ProtectedData": "9.0.0", - "System.Security.Cryptography.Xml": "9.0.0" - } - }, - "Microsoft.Build.Utilities.Core": { - "type": "Transitive", - "resolved": "17.14.28", - "contentHash": "rhSdPo8QfLXXWM+rY0x0z1G4KK4ZhMoIbHROyDj8MUBFab9nvHR0NaMnjzOgXldhmD2zi2ir8d6xCatNzlhF5g==", - "dependencies": { - "Microsoft.Build.Framework": "17.14.28", - "Microsoft.NET.StringTools": "17.14.28", - "System.Collections.Immutable": "9.0.0", - "System.Configuration.ConfigurationManager": "9.0.0", - "System.Diagnostics.EventLog": "9.0.0", - "System.Security.Cryptography.ProtectedData": "9.0.0" - } - }, - "Microsoft.CodeAnalysis.Analyzers": { - "type": "Transitive", - "resolved": "3.11.0", - "contentHash": "v/EW3UE8/lbEYHoC2Qq7AR/DnmvpgdtAMndfQNmpuIMx/Mto8L5JnuCfdBYtgvalQOtfNCnxFejxuRrryvUTsg==" - }, - "Microsoft.CodeAnalysis.Common": { - "type": "Transitive", - "resolved": "4.14.0", - "contentHash": "PC3tuwZYnC+idaPuoC/AZpEdwrtX7qFpmnrfQkgobGIWiYmGi5MCRtl5mx6QrfMGQpK78X2lfIEoZDLg/qnuHg==", - "dependencies": { - "Microsoft.CodeAnalysis.Analyzers": "3.11.0", - "System.Collections.Immutable": "9.0.0", - "System.Reflection.Metadata": "9.0.0" - } - }, - "Microsoft.CodeAnalysis.CSharp": { - "type": "Transitive", - "resolved": "4.14.0", - "contentHash": "568a6wcTivauIhbeWcCwfWwIn7UV7MeHEBvFB2uzGIpM2OhJ4eM/FZ8KS0yhPoNxnSpjGzz7x7CIjTxhslojQA==", - "dependencies": { - "Microsoft.CodeAnalysis.Analyzers": "3.11.0", - "Microsoft.CodeAnalysis.Common": "[4.14.0]", - "System.Collections.Immutable": "9.0.0", - "System.Reflection.Metadata": "9.0.0" - } - }, - "Microsoft.CodeAnalysis.CSharp.Workspaces": { - "type": "Transitive", - "resolved": "4.14.0", - "contentHash": "QkgCEM4qJo6gdtblXtNgHqtykS61fxW+820hx5JN6n9DD4mQtqNB+6fPeJ3GQWg6jkkGz6oG9yZq7H3Gf0zwYw==", - "dependencies": { - "Humanizer.Core": "2.14.1", - "Microsoft.CodeAnalysis.Analyzers": "3.11.0", - "Microsoft.CodeAnalysis.CSharp": "[4.14.0]", - "Microsoft.CodeAnalysis.Common": "[4.14.0]", - "Microsoft.CodeAnalysis.Workspaces.Common": "[4.14.0]", - "System.Collections.Immutable": "9.0.0", - "System.Composition": "9.0.0", - "System.IO.Pipelines": "9.0.0", - "System.Reflection.Metadata": "9.0.0", - "System.Threading.Channels": "7.0.0" - } - }, - "Microsoft.CodeAnalysis.Workspaces.Common": { - "type": "Transitive", - "resolved": "4.14.0", - "contentHash": "wNVK9JrqjqDC/WgBUFV6henDfrW87NPfo98nzah/+M/G1D6sBOPtXwqce3UQNn+6AjTnmkHYN1WV9XmTlPemTw==", - "dependencies": { - "Humanizer.Core": "2.14.1", - "Microsoft.CodeAnalysis.Analyzers": "3.11.0", - "Microsoft.CodeAnalysis.Common": "[4.14.0]", - "System.Collections.Immutable": "9.0.0", - "System.Composition": "9.0.0", - "System.IO.Pipelines": "9.0.0", - "System.Reflection.Metadata": "9.0.0", - "System.Threading.Channels": "7.0.0" - } - }, - "Microsoft.CodeAnalysis.Workspaces.MSBuild": { - "type": "Transitive", - "resolved": "4.14.0", - "contentHash": "YU7Sguzm1Cuhi2U6S0DRKcVpqAdBd2QmatpyE0KqYMJogJ9E27KHOWGUzAOjsyjAM7sNaUk+a8VPz24knDseFw==", - "dependencies": { - "Humanizer.Core": "2.14.1", - "Microsoft.Build": "17.7.2", - "Microsoft.Build.Framework": "17.7.2", - "Microsoft.Build.Tasks.Core": "17.7.2", - "Microsoft.Build.Utilities.Core": "17.7.2", - "Microsoft.CodeAnalysis.Analyzers": "3.11.0", - "Microsoft.CodeAnalysis.Workspaces.Common": "[4.14.0]", - "Microsoft.Extensions.DependencyInjection": "9.0.0", - "Microsoft.Extensions.Logging": "9.0.0", - "Microsoft.Extensions.Logging.Abstractions": "9.0.0", - "Microsoft.Extensions.Options": "9.0.0", - "Microsoft.Extensions.Primitives": "9.0.0", - "Newtonsoft.Json": "13.0.3", - "System.CodeDom": "7.0.0", - "System.Collections.Immutable": "9.0.0", - "System.Composition": "9.0.0", - "System.Configuration.ConfigurationManager": "9.0.0", - "System.Diagnostics.EventLog": "9.0.0", - "System.IO.Pipelines": "9.0.0", - "System.Reflection.Metadata": "9.0.0", - "System.Resources.Extensions": "9.0.0", - "System.Security.Cryptography.Pkcs": "7.0.2", - "System.Security.Cryptography.ProtectedData": "9.0.0", - "System.Security.Cryptography.Xml": "7.0.1", - "System.Security.Permissions": "9.0.0", - "System.Text.Json": "9.0.0", - "System.Threading.Channels": "7.0.0", - "System.Threading.Tasks.Dataflow": "9.0.0", - "System.Windows.Extensions": "9.0.0" - } - }, - "Microsoft.Extensions.DependencyInjection": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "MCPrg7v3QgNMr0vX4vzRXvkNGgLg8vKWX0nKCWUxu2uPyMsaRgiRc1tHBnbTcfJMhMKj2slE/j2M9oGkd25DNw==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0" - } - }, - "Microsoft.Extensions.DependencyInjection.Abstractions": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "+6f2qv2a3dLwd5w6JanPIPs47CxRbnk+ZocMJUhv9NxP88VlOcJYZs9jY+MYSjxvady08bUZn6qgiNh7DadGgg==" - }, - "Microsoft.Extensions.Logging": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "crjWyORoug0kK7RSNJBTeSE6VX8IQgLf3nUpTB9m62bPXp/tzbnOsnbe8TXEG0AASNaKZddnpHKw7fET8E++Pg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection": "9.0.0", - "Microsoft.Extensions.Logging.Abstractions": "9.0.0", - "Microsoft.Extensions.Options": "9.0.0" - } - }, - "Microsoft.Extensions.Logging.Abstractions": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "g0UfujELzlLbHoVG8kPKVBaW470Ewi+jnptGS9KUi6jcb+k2StujtK3m26DFSGGwQ/+bVgZfsWqNzlP6YOejvw==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0" - } - }, - "Microsoft.Extensions.Options": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "y2146b3jrPI3Q0lokKXdKLpmXqakYbDIPDV6r3M8SqvSf45WwOTzkyfDpxnZXJsJQEpAsAqjUq5Pu8RCJMjubg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", - "Microsoft.Extensions.Primitives": "9.0.0" - } - }, - "Microsoft.Extensions.Primitives": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "N3qEBzmLMYiASUlKxxFIISP4AiwuPTHF5uCh+2CWSwwzAJiIYx0kBJsS30cp1nvhSySFAVi30jecD307jV+8Kg==" - }, - "Microsoft.NET.StringTools": { - "type": "Transitive", - "resolved": "17.14.28", - "contentHash": "DMIeWDlxe0Wz0DIhJZ2FMoGQAN2yrGZOi5jjFhRYHWR5ONd0CS6IpAHlRnA7uA/5BF+BADvgsETxW2XrPiFc1A==" - }, - "Microsoft.Testing.Extensions.TrxReport.Abstractions": { - "type": "Transitive", - "resolved": "2.0.1", - "contentHash": "GCyRVLdFiOlGkgZItt2kHJRn/NY9zhEF5EQwjFCOl6fwr+uSy0KtwwGHF5uERevjEEBTqRbj4wwrraAqRyq1Zw==", - "dependencies": { - "Microsoft.Testing.Platform": "2.0.1" - } - }, - "Microsoft.Testing.Platform": { - "type": "Transitive", - "resolved": "2.0.1", - "contentHash": "k1k9DV/pzkhiG0LOokmrBf63BBhgUoHcjsOA6C0S26hiO+prcUIw4wpbzrUGrwMmJlo1P0idxJtiFEAZdvNwxQ==" - }, - "Microsoft.Testing.Platform.MSBuild": { - "type": "Transitive", - "resolved": "2.0.1", - "contentHash": "zI8svKM2d1wAu20u+cMRo13oJv4uyPcCmVZixvKk+W34d1F7hH2msEzyQ4zv3NEliE60JMM9cdDxWcA3yU2oAA==", - "dependencies": { - "Microsoft.Testing.Platform": "2.0.1" - } - }, - "Mono.Cecil": { - "type": "Transitive", - "resolved": "0.11.6", - "contentHash": "f33RkDtZO8VlGXCtmQIviOtxgnUdym9xx/b1p9h91CRGOsJFxCFOFK1FDbVt1OCf1aWwYejUFa2MOQyFWTFjbA==" - }, - "MonoMod.Backports": { - "type": "Transitive", - "resolved": "1.1.2", - "contentHash": "baYlNy8n8kmaNhNvqmZ/dIPOeO1r9//dG1i2WbunMWtWZ2EKtIgmXaS+ZzphzTsikkGnoD4Jwr5g0TVdpDjgpw==", - "dependencies": { - "MonoMod.ILHelpers": "1.1.0" - } - }, - "MonoMod.Core": { - "type": "Transitive", - "resolved": "1.3.1", - "contentHash": "AE78s2Iv74mFfkbH+iUAGmVpf+zkMlJYOtQPVVuRN4Eorcy7Dm4wps1X/CVp4p6a7MnbCKOuQz9OeVC/xJN6+Q==", - "dependencies": { - "Mono.Cecil": "0.11.6", - "MonoMod.Backports": "1.1.2", - "MonoMod.ILHelpers": "1.1.0", - "MonoMod.Utils": "25.0.9" - } - }, - "MonoMod.ILHelpers": { - "type": "Transitive", - "resolved": "1.1.0", - "contentHash": "L2FWjhTrv7tcIxshfZ+M3OcaNr4cNw0IwiVZEgwqRnZ5QAN3+RrNJ8ZwCzwXUWyPDqooJxMcjjg8PsSYUiNBjQ==" - }, - "MonoMod.RuntimeDetour": { - "type": "Transitive", - "resolved": "25.3.1", - "contentHash": "QQ9Ng3E6enCMbcFNDSUqWBD4+KdnjfxConI8QzMAH9p7i2vvzehqYT8K4Mghq2YeTIVN6Aih8PlqcQxQk2uypQ==", - "dependencies": { - "Mono.Cecil": "0.11.6", - "MonoMod.Backports": "1.1.2", - "MonoMod.Core": "1.3.1", - "MonoMod.ILHelpers": "1.1.0", - "MonoMod.Utils": "25.0.9" - } - }, - "MonoMod.Utils": { - "type": "Transitive", - "resolved": "25.0.9", - "contentHash": "KPZ/VAr4zwT12/OJPZF2uazc9M/tRjiizeErq4m5Ie4EA6XnreakLfs8RlvxszgwXL7FVGnkg9JU5tKtQRPGMA==", - "dependencies": { - "Mono.Cecil": "0.11.6", - "MonoMod.Backports": "1.1.2", - "MonoMod.ILHelpers": "1.1.0" - } - }, - "Newtonsoft.Json": { - "type": "Transitive", - "resolved": "13.0.3", - "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" - }, - "StyleCop.Analyzers.Unstable": { - "type": "Transitive", - "resolved": "1.2.0.556", - "contentHash": "zvn9Mqs/ox/83cpYPignI8hJEM2A93s2HkHs8HYMOAQW0PkampyoErAiIyKxgTLqbbad29HX/shv/6LGSjPJNQ==" - }, - "System.CodeDom": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "oTE5IfuMoET8yaZP/vdvy9xO47guAv/rOhe4DODuFBN3ySprcQOlXqO3j+e/H/YpKKR5sglrxRaZ2HYOhNJrqA==" - }, - "System.Collections.Immutable": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "QhkXUl2gNrQtvPmtBTQHb0YsUrDiDQ2QS09YbtTTiSjGcf7NBqtYbrG/BE06zcBPCKEwQGzIv13IVdXNOSub2w==" - }, - "System.Composition": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "3Djj70fFTraOarSKmRnmRy/zm4YurICm+kiCtI0dYRqGJnLX6nJ+G3WYuFJ173cAPax/gh96REcbNiVqcrypFQ==", - "dependencies": { - "System.Composition.AttributedModel": "9.0.0", - "System.Composition.Convention": "9.0.0", - "System.Composition.Hosting": "9.0.0", - "System.Composition.Runtime": "9.0.0", - "System.Composition.TypedParts": "9.0.0" - } - }, - "System.Composition.AttributedModel": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "iri00l/zIX9g4lHMY+Nz0qV1n40+jFYAmgsaiNn16xvt2RDwlqByNG4wgblagnDYxm3YSQQ0jLlC/7Xlk9CzyA==" - }, - "System.Composition.Convention": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "+vuqVP6xpi582XIjJi6OCsIxuoTZfR0M7WWufk3uGDeCl3wGW6KnpylUJ3iiXdPByPE0vR5TjJgR6hDLez4FQg==", - "dependencies": { - "System.Composition.AttributedModel": "9.0.0" - } - }, - "System.Composition.Hosting": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "OFqSeFeJYr7kHxDfaViGM1ymk7d4JxK//VSoNF9Ux0gpqkLsauDZpu89kTHHNdCWfSljbFcvAafGyBoY094btQ==", - "dependencies": { - "System.Composition.Runtime": "9.0.0" - } - }, - "System.Composition.Runtime": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "w1HOlQY1zsOWYussjFGZCEYF2UZXgvoYnS94NIu2CBnAGMbXFAX8PY8c92KwUItPmowal68jnVLBCzdrWLeEKA==" - }, - "System.Composition.TypedParts": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "aRZlojCCGEHDKqh43jaDgaVpYETsgd7Nx4g1zwLKMtv4iTo0627715ajEFNpEEBTgLmvZuv8K0EVxc3sM4NWJA==", - "dependencies": { - "System.Composition.AttributedModel": "9.0.0", - "System.Composition.Hosting": "9.0.0", - "System.Composition.Runtime": "9.0.0" - } - }, - "System.Configuration.ConfigurationManager": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "PdkuMrwDhXoKFo/JxISIi9E8L+QGn9Iquj2OKDWHB6Y/HnUOuBouF7uS3R4Hw3FoNmwwMo6hWgazQdyHIIs27A==", - "dependencies": { - "System.Diagnostics.EventLog": "9.0.0", - "System.Security.Cryptography.ProtectedData": "9.0.0" - } - }, - "System.Diagnostics.EventLog": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "qd01+AqPhbAG14KtdtIqFk+cxHQFZ/oqRSCoxU1F+Q6Kv0cl726sl7RzU9yLFGd4BUOKdN4XojXF0pQf/R6YeA==" - }, - "System.Formats.Nrbf": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "F/6tNE+ckmdFeSQAyQo26bQOqfPFKEfZcuqnp4kBE6/7jP26diP+QTHCJJ6vpEfaY6bLy+hBLiIQUSxSmNwLkA==" - }, - "System.IO.Pipelines": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "eA3cinogwaNB4jdjQHOP3Z3EuyiDII7MT35jgtnsA4vkn0LUrrSHsU0nzHTzFzmaFYeKV7MYyMxOocFzsBHpTw==" - }, - "System.Reflection.Metadata": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "ANiqLu3DxW9kol/hMmTWbt3414t9ftdIuiIU7j80okq2YzAueo120M442xk1kDJWtmZTqWQn7wHDvMRipVOEOQ==" - }, - "System.Reflection.MetadataLoadContext": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "nGdCUVhEQ9/CWYqgaibYEDwIJjokgIinQhCnpmtZfSXdMS6ysLZ8p9xvcJ8VPx6Xpv5OsLIUrho4B9FN+VV/tw==" - }, - "System.Resources.Extensions": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "tvhuT1D2OwPROdL1kRWtaTJliQo0WdyhvwDpd8RM997G7m3Hya5nhbYhNTS75x6Vu+ypSOgL5qxDCn8IROtCxw==", - "dependencies": { - "System.Formats.Nrbf": "9.0.0" - } - }, - "System.Security.Cryptography.Pkcs": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "8tluJF8w9si+2yoHeL8rgVJS6lKvWomTDC8px65Z8MCzzdME5eaPtEQf4OfVGrAxB5fW93ncucy1+221O9EQaw==" - }, - "System.Security.Cryptography.ProtectedData": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "CJW+x/F6fmRQ7N6K8paasTw9PDZp4t7G76UjGNlSDgoHPF0h08vTzLYbLZpOLEJSg35d5wy2jCXGo84EN05DpQ==" - }, - "System.Security.Cryptography.Xml": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "GQZn5wFd+pyOfwWaCbqxG7trQ5ox01oR8kYgWflgtux4HiUNihGEgG2TktRWyH+9bw7NoEju1D41H/upwQeFQw==", - "dependencies": { - "System.Security.Cryptography.Pkcs": "9.0.0" - } - }, - "System.Security.Permissions": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "H2VFD4SFVxieywNxn9/epb63/IOcPPfA0WOtfkljzNfu7GCcHIBQNuwP6zGCEIi7Ci/oj8aLPUNK9sYImMFf4Q==", - "dependencies": { - "System.Windows.Extensions": "9.0.0" - } - }, - "System.Text.Json": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "js7+qAu/9mQvnhA4EfGMZNEzXtJCDxgkgj8ohuxq/Qxv+R56G+ljefhiJHOxTNiw54q8vmABCWUwkMulNdlZ4A==" - }, - "System.Threading.Channels": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "qmeeYNROMsONF6ndEZcIQ+VxR4Q/TX/7uIVLJqtwIWL7dDWeh0l1UIqgo4wYyjG//5lUNhwkLDSFl+pAWO6oiA==" - }, - "System.Threading.Tasks.Dataflow": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "S+y+QuBJNcqOvoFK+rFcZZuQDlD2E4lImKW9/g3E0l7YT2uo4oin9amAn398eGt/xFBYNNSt5O77Dbc38XGfBw==" - }, - "System.Windows.Extensions": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "U9msthvnH2Fsw7xwAvIhNHOdnIjOQTwOc8Vd0oGOsiRcGMGoBFlUD6qtYawRUoQdKH9ysxesZ9juFElt1Jw/7A==" - }, - "TUnit.Assertions": { - "type": "Transitive", - "resolved": "0.75.30", - "contentHash": "mVxJ9U8SlUPcm2/+Z7wiVPxnWFLzZ7ktM8UHZxY4KV0ydxhGSWyrAjOaaifR0N4grPLZJSqniNvG4NV3OMSJ4w==" - }, - "TUnit.Core": { - "type": "Transitive", - "resolved": "0.75.30", - "contentHash": "XRIISUsTTfAvSpD3nOBSCmYJWGl9D21/eDkJl4g/qYBoT9P8KnqDyuQO14UZxMyZR5uTDVC0arnOv8DYb8YQrg==" - }, - "TUnit.Engine": { - "type": "Transitive", - "resolved": "0.75.30", - "contentHash": "0M6OVdfcjjCDmzA8pAzpVgwDcGotun5uMPHK8BsbFGn6Yyf2t7I5Hb1Ke68evOQbNzcQc77yieiRYMzMKAbU8A==", - "dependencies": { - "EnumerableAsyncProcessor": "3.8.4", - "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.0.1", - "Microsoft.Testing.Platform": "2.0.1", - "Microsoft.Testing.Platform.MSBuild": "2.0.1", - "TUnit.Core": "0.75.30" - } - }, - "compatunbreaker": { - "type": "Project", - "dependencies": { - "AsmResolver.DotNet": "[6.0.0-dev, )", - "CompatUnbreaker.Attributes": "[1.0.0-dev, )", - "Meziantou.Analyzer": "[2.0.220, )", - "PolySharp": "[1.15.0, )", - "StyleCop.Analyzers": "[1.2.0-beta.556, )" - } - }, - "compatunbreaker.attributes": { - "type": "Project", - "dependencies": { - "Meziantou.Analyzer": "[2.0.220, )", - "PolySharp": "[1.15.0, )", - "StyleCop.Analyzers": "[1.2.0-beta.556, )" - } - }, - "compatunbreaker.tool": { - "type": "Project", - "dependencies": { - "Basic.Reference.Assemblies.Net90": "[1.8.3, )", - "CompatUnbreaker": "[1.0.0-dev, )", - "Meziantou.Analyzer": "[2.0.220, )", - "Microsoft.Build": "[17.14.28, )", - "Microsoft.Build.Framework": "[17.14.28, )", - "Microsoft.Build.Locator": "[1.10.2, )", - "Microsoft.Build.Tasks.Core": "[17.14.28, )", - "Microsoft.Build.Utilities.Core": "[17.14.28, )", - "Microsoft.CodeAnalysis.CSharp": "[4.14.0, )", - "Microsoft.CodeAnalysis.CSharp.Workspaces": "[4.14.0, )", - "Microsoft.CodeAnalysis.Workspaces.MSBuild": "[4.14.0, )", - "MonoMod.RuntimeDetour": "[25.3.1, )", - "PolySharp": "[1.15.0, )", - "StyleCop.Analyzers": "[1.2.0-beta.556, )" - } - } - } - } -} \ No newline at end of file diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/Commands/BaseShimProjectCommand.cs b/src/build/CompatUnbreaker/CompatUnbreaker.Tool/Commands/BaseShimProjectCommand.cs deleted file mode 100644 index 2beb9fa..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/Commands/BaseShimProjectCommand.cs +++ /dev/null @@ -1,112 +0,0 @@ -using AsmResolver.DotNet; -using CompatUnbreaker.Models; -using CompatUnbreaker.Processors; -using CompatUnbreaker.Processors.Abstractions; -using CompatUnbreaker.Tool.ApiCompatibility.AssemblyMapping; -using CompatUnbreaker.Tool.ApiCompatibility.Comparing; -using CompatUnbreaker.Tool.SkeletonGeneration; -using CompatUnbreaker.Utilities.AsmResolver; -using ConsoleAppFramework; -using Microsoft.Build.Execution; -using Microsoft.Build.Framework; -using Microsoft.Build.Locator; -using Microsoft.Build.Logging; - -namespace CompatUnbreaker.Tool.Commands; - -public abstract class BaseShimProjectCommand -{ - static BaseShimProjectCommand() - { - MSBuildLocator.RegisterDefaults(); - } - - private static (List FrameworkReferences, List References) BuildAndGetReferences(ref ProjectInstance project, bool build) - { - var loggers = new ConsoleLogger[] - { - new ConsoleLogger(LoggerVerbosity.Quiet), - }; - - var targetFrameworks = project.GetPropertyValue("TargetFrameworks"); - if (!string.IsNullOrEmpty(targetFrameworks)) - { - project = new ProjectInstance(project.FullPath, new Dictionary - { - ["TargetFramework"] = targetFrameworks.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)[0], - }, null); - } - - if (!build) - { - project.SetProperty("BuildProjectReferences", "false"); - } - - if (!project.Build("Restore", loggers)) - { - throw new Exception("Failed to restore packages"); - } - - if (!project.Build(build ? "Build" : "FindReferenceAssembliesForReferences", loggers)) - { - throw new Exception("Failed to build"); - } - - var frameworkReferences = new List(); - var references = new List(); - - foreach (var reference in project.GetItems("ReferencePathWithRefAssemblies")) - { - if (reference.HasMetadata("FrameworkReferenceName") || reference.GetMetadataValue("NuGetPackageId") == "NETStandard.Library") - { - frameworkReferences.Add(reference.EvaluatedInclude); - } - else - { - references.Add(reference.EvaluatedInclude); - } - } - - return (frameworkReferences, references); - } - - protected async Task LoadAsync([Argument] string projectPath, bool skipBuild = false) - { - var shimProject = new ProjectInstance(projectPath); - - var shimTargetAssemblyName = shimProject.GetPropertyValue("ShimTargetAssemblyName"); - var shimBaselineProjectPath = shimProject.GetPropertyValue("ShimBaselineProject"); - - var shimBaselineProject = new ProjectInstance(Path.Combine(shimProject.Directory, shimBaselineProjectPath)); - - var (shimFrameworkReferences, shimReferences) = BuildAndGetReferences(ref shimProject, !skipBuild); - var (baselineFrameworkReferences, baselineReferences) = BuildAndGetReferences(ref shimBaselineProject, false); - - var shimPath = shimProject.GetPropertyValue("TargetPath"); - - baselineReferences.RemoveAll(r => shimFrameworkReferences.Any(r2 => Path.GetFileName(r2) == Path.GetFileName(r))); - - var shimReferenceAssemblies = shimFrameworkReferences.Concat(shimReferences).Select(AssemblyDefinition.FromFile).ToArray(); - var baselineReferenceAssemblies = shimFrameworkReferences.Concat(baselineReferences).Select(AssemblyDefinition.FromFile).ToArray(); - - var shimAssembly = new SimpleAssemblyResolver(shimReferenceAssemblies).Load(shimPath); - var targetAssembly = shimReferenceAssemblies.Single(a => a.Name == shimTargetAssemblyName); - - var baselineResolver = new SimpleAssemblyResolver(baselineReferenceAssemblies); - var baselineAssembly = baselineReferenceAssemblies.Single(a => a.Name == shimTargetAssemblyName); - - foreach (var assemblyReference in baselineAssembly.Modules.Concat(targetAssembly.Modules).SelectMany(m => m.AssemblyReferences)) - { - if (assemblyReference.Resolve() == null) - { - throw new InvalidOperationException($"Couldn't resolve {assemblyReference}"); - } - } - - Unbreaker.ProcessReference(shimAssembly, targetAssembly); - - var assemblyMapper = AssemblyMapper.Create(baselineAssembly, targetAssembly); - - return assemblyMapper; - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/Commands/CompareCommand.cs b/src/build/CompatUnbreaker/CompatUnbreaker.Tool/Commands/CompareCommand.cs deleted file mode 100644 index 9dfc662..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/Commands/CompareCommand.cs +++ /dev/null @@ -1,21 +0,0 @@ -using CompatUnbreaker.Tool.ApiCompatibility.Comparing; -using ConsoleAppFramework; - -namespace CompatUnbreaker.Tool.Commands; - -internal sealed class CompareCommand : BaseShimProjectCommand -{ - [Command("compare")] - public async Task ExecuteAsync([Argument] string projectPath, bool skipBuild = false) - { - var assemblyMapper = await LoadAsync(projectPath, skipBuild); - - var apiComparer = new ApiComparer(); - apiComparer.Compare(assemblyMapper); - - foreach (var difference in apiComparer.CompatDifferences) - { - Console.WriteLine(difference); - } - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/Commands/SkeletonCommand.cs b/src/build/CompatUnbreaker/CompatUnbreaker.Tool/Commands/SkeletonCommand.cs deleted file mode 100644 index 977f940..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/Commands/SkeletonCommand.cs +++ /dev/null @@ -1,15 +0,0 @@ -using CompatUnbreaker.Tool.SkeletonGeneration; -using ConsoleAppFramework; - -namespace CompatUnbreaker.Tool.Commands; - -internal sealed class SkeletonCommand : BaseShimProjectCommand -{ - [Command("skeleton")] - public async Task ExecuteAsync([Argument] string projectPath, bool skipBuild = false) - { - var assemblyMapper = await LoadAsync(projectPath, skipBuild); - - await SkeletonGenerator.GenerateAsync(assemblyMapper, projectPath); - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/CompatUnbreaker.Tool.csproj b/src/build/CompatUnbreaker/CompatUnbreaker.Tool/CompatUnbreaker.Tool.csproj deleted file mode 100644 index b145b28..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/CompatUnbreaker.Tool.csproj +++ /dev/null @@ -1,35 +0,0 @@ - - - net$(NETCoreAppMaximumVersion) - Exe - - - - - - - - - - - - - - - - - - - - - - - - - - - - false - - - diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/Program.cs b/src/build/CompatUnbreaker/CompatUnbreaker.Tool/Program.cs deleted file mode 100644 index 5e8d32e..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/Program.cs +++ /dev/null @@ -1,17 +0,0 @@ -using CompatUnbreaker.Tool.Commands; -using ConsoleAppFramework; - -namespace CompatUnbreaker.Tool; - -internal sealed class Program -{ - public static async Task Main(string[] args) - { - var app = ConsoleApp.Create(); - - app.Add(); - app.Add(); - - await app.RunAsync(args); - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/AsmResolverTypeSyntaxGenerator.cs b/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/AsmResolverTypeSyntaxGenerator.cs deleted file mode 100644 index 80ac0cd..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/AsmResolverTypeSyntaxGenerator.cs +++ /dev/null @@ -1,377 +0,0 @@ -using System.Diagnostics; -using AsmResolver.DotNet; -using AsmResolver.DotNet.Signatures; -using CompatUnbreaker.Tool.Utilities; -using CompatUnbreaker.Tool.Utilities.AsmResolver; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Simplification; -using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; - -namespace CompatUnbreaker.Tool.SkeletonGeneration.CodeGeneration; - -internal sealed class AsmResolverTypeSyntaxGenerator : ITypeSignatureVisitor -{ - private static AsmResolverTypeSyntaxGenerator Instance { get; } = new(); - - private static TTypeSyntax AddInformationTo(TTypeSyntax syntax, ITypeDescriptor symbol) - where TTypeSyntax : TypeSyntax - { - // syntax = syntax.WithPrependedLeadingTrivia(ElasticMarker).WithAppendedTrailingTrivia(ElasticMarker); - // syntax = syntax.WithAdditionalAnnotations(SymbolAnnotation.Create(symbol)); - - return syntax; - } - - public static TypeSyntax TypeExpression(ITypeDescriptor type, TypeContext context) - { - return Instance.VisitTypeDescriptor(type, context); - } - - private TypeSyntax VisitTypeDescriptor(ITypeDescriptor type, TypeContext context) => type switch - { - TypeSignature signature => signature.AcceptVisitor(this, context), - ITypeDefOrRef reference => VisitTypeDefOrRef(reference, context), - ExportedType exported => VisitSimpleType(exported, context), - _ => throw new ArgumentOutOfRangeException(nameof(type)), - }; - - private TypeSyntax VisitTypeDefOrRef(ITypeDefOrRef type, TypeContext context) => type switch - { - TypeSpecification specification => VisitTypeDescriptor(specification.Signature, context), - _ => VisitSimpleType(type, context), - }; - - private TypeSyntax VisitSimpleType(ITypeDescriptor symbol, TypeContext context, IList? typeArguments = null) - { - var name = MetadataHelpers.InferTypeArityAndUnmangleMetadataName(symbol.Name, out var arity).ToString(); - - var isNonGenericValueType = arity > 0 - ? symbol.IsTypeOf("System", "Nullable`1") - : symbol.IsValueType; - - var nullableAnnotation = isNonGenericValueType - ? NullableAnnotation.Oblivious - : context.Transform.TryConsumeNullableTransform() ?? NullableAnnotation.Oblivious; - - if (context.Transform.TryConsumeDynamicTransform() == true) - { - return IdentifierName("dynamic"); - } - - if (typeArguments != null && IsTupleTypeOfCardinality(new GenericInstanceTypeSignature(symbol.ToTypeDefOrRef(), true, typeArguments.ToArray()), out var cardinality)) - { - var names = context.Transform.ConsumeTupleElementNames(cardinality); - - var elements = default(SeparatedSyntaxList); - - for (var i = 0; i < Math.Min(typeArguments.Count, ValueTupleRestPosition - 1); i++) - { - var elementTypeSignature = typeArguments[i]; - var elementType = elementTypeSignature.AcceptVisitor(this, context); - - elements = elements.Add( - names.IsEmpty - ? TupleElement(elementType) - : TupleElement(elementType, names[i]?.ToIdentifierToken() ?? default) - ); - } - - if (typeArguments.Count >= ValueTupleRestPosition) - { - var rest = (TupleTypeSyntax) typeArguments[ValueTupleRestPosition - 1].AcceptVisitor(this, context); - - for (var i = 0; i < rest.Elements.Count; i++) - { - var element = rest.Elements[i]; - - elements = elements.Add( - names.IsEmpty - ? element - : element.WithIdentifier(names[ValueTupleRestPosition - 1 + i]?.ToIdentifierToken() ?? default) - ); - } - } - - return TupleType(elements); - } - - SimpleNameSyntax simpleNameSyntax; - - if (arity == 0) - { - simpleNameSyntax = name.ToIdentifierName(); - } - else - { - simpleNameSyntax = GenericName( - name.ToIdentifierToken(), - TypeArgumentList([ - .. typeArguments == null - ? Enumerable.Repeat(OmittedTypeArgument(), arity) - : typeArguments.Select(t => VisitTypeDescriptor(t, context)), - ]) - ); - } - - TypeSyntax typeSyntax; - - if (symbol.DeclaringType != null) - { - var declaringTypeSyntax = VisitTypeDescriptor(symbol.DeclaringType, context); - if (declaringTypeSyntax is NameSyntax declaringTypeName) - { - typeSyntax = QualifiedName(declaringTypeName, simpleNameSyntax); - } - else - { - typeSyntax = simpleNameSyntax; - } - - typeSyntax = AddInformationTo(typeSyntax, symbol); - } - else - { - typeSyntax = AddInformationTo(AddNamespace(symbol.Namespace, simpleNameSyntax), symbol); - } - - if (nullableAnnotation == NullableAnnotation.Annotated) - { - typeSyntax = AddInformationTo(NullableType(typeSyntax), symbol); - } - - return typeSyntax; - } - - private const int ValueTupleRestPosition = 8; - - private static bool IsTupleTypeOfCardinality(TypeSignature signature, out int tupleCardinality) - { - if (signature is GenericInstanceTypeSignature genericSignature && - genericSignature.Namespace == "System" && - genericSignature.GetUnmangledName() is "ValueTuple") - { - var arity = genericSignature.TypeArguments.Count; - - if (arity is >= 0 and < ValueTupleRestPosition) - { - tupleCardinality = arity; - return true; - } - - if (arity == ValueTupleRestPosition) - { - var typeToCheck = genericSignature; - var levelsOfNesting = 0; - - do - { - levelsOfNesting++; - typeToCheck = (GenericInstanceTypeSignature) typeToCheck.TypeArguments[ValueTupleRestPosition - 1]; - } while (SignatureComparer.Default.Equals(typeToCheck.GenericType, genericSignature.GenericType)); - - arity = typeToCheck.TypeArguments.Count; - - if (arity is > 0 and < ValueTupleRestPosition && IsTupleTypeOfCardinality(typeToCheck, out tupleCardinality)) - { - Debug.Assert(tupleCardinality < ValueTupleRestPosition); - tupleCardinality += (ValueTupleRestPosition - 1) * levelsOfNesting; - return true; - } - } - } - - tupleCardinality = 0; - return false; - } - - private static NameSyntax AddNamespace(string? @namespace, SimpleNameSyntax name) - { - if (string.IsNullOrEmpty(@namespace)) - { - return AddGlobalAlias(name); - } - - var parts = @namespace.Split('.'); - NameSyntax namespaceName = parts[0].ToIdentifierName(); - for (var i = 1; i < parts.Length; i++) - { - namespaceName = QualifiedName(namespaceName, parts[i].ToIdentifierName()); - } - - return QualifiedName(ParseName(@namespace), name); - } - - private static AliasQualifiedNameSyntax AddGlobalAlias(SimpleNameSyntax syntax) - { - return AliasQualifiedName(CreateGlobalIdentifier(), syntax); - } - - private static IdentifierNameSyntax CreateGlobalIdentifier() => IdentifierName(Token(SyntaxKind.GlobalKeyword)); - - public TypeSyntax VisitBoxedType(BoxedTypeSignature signature, TypeContext context) - { - throw new NotImplementedException(); - } - - public TypeSyntax VisitByReferenceType(ByReferenceTypeSignature signature, TypeContext context) - { - return RefType(signature.BaseType.AcceptVisitor(this, context)); - } - - public TypeSyntax VisitCorLibType(CorLibTypeSignature signature, TypeContext context) - { - // SyntaxKind? kind = signature.ElementType switch - // { - // ElementType.Boolean => SyntaxKind.BoolKeyword, - // ElementType.U1 => SyntaxKind.ByteKeyword, - // ElementType.I1 => SyntaxKind.SByteKeyword, - // ElementType.I4 => SyntaxKind.IntKeyword, - // ElementType.U4 => SyntaxKind.UIntKeyword, - // ElementType.I2 => SyntaxKind.ShortKeyword, - // ElementType.U2 => SyntaxKind.UShortKeyword, - // ElementType.I8 => SyntaxKind.LongKeyword, - // ElementType.U8 => SyntaxKind.ULongKeyword, - // ElementType.R4 => SyntaxKind.FloatKeyword, - // ElementType.R8 => SyntaxKind.DoubleKeyword, - // ElementType.String => SyntaxKind.StringKeyword, - // ElementType.Char => SyntaxKind.CharKeyword, - // ElementType.Object => SyntaxKind.ObjectKeyword, - // ElementType.Void => SyntaxKind.VoidKeyword, - // _ => null, - // }; - // - // if (kind != null) - // { - // return PredefinedType(Token(kind.Value)); - // } - - return VisitTypeDefOrRef(signature.Type, context); - } - - public TypeSyntax VisitCustomModifierType(CustomModifierTypeSignature signature, TypeContext context) - { - return signature.BaseType.AcceptVisitor(this, context); - } - - public TypeSyntax VisitGenericInstanceType(GenericInstanceTypeSignature signature, TypeContext context) - { - return VisitSimpleType(signature.GenericType, context, signature.TypeArguments); - } - - public TypeSyntax VisitGenericParameter(GenericParameterSignature signature, TypeContext context) - { - var nullableAnnotation = context.Transform.TryConsumeNullableTransform(); - if (context.Transform.TryConsumeDynamicTransform() == true) - { - return IdentifierName("dynamic"); - } - - var genericParameter = context.Generic.GetGenericParameter(signature); - - TypeSyntax result = AddInformationTo( - genericParameter != null - ? genericParameter.Name.Value.ToIdentifierName() - : signature.Name.ToIdentifierName(), - signature - ); - - return nullableAnnotation == NullableAnnotation.Annotated ? AddInformationTo(NullableType(result), signature) : result; - } - - public TypeSyntax VisitPinnedType(PinnedTypeSignature signature, TypeContext context) - { - throw new NotImplementedException(); - } - - public TypeSyntax VisitPointerType(PointerTypeSignature signature, TypeContext context) - { - return PointerType(signature.BaseType.AcceptVisitor(this, context)); - } - - public TypeSyntax VisitSentinelType(SentinelTypeSignature signature, TypeContext context) - { - throw new NotImplementedException(); - } - - public TypeSyntax VisitSzArrayType(SzArrayTypeSignature signature, TypeContext context) - { - var nullableAnnotation = context.Transform.TryConsumeNullableTransform(); - if (context.Transform.TryConsumeDynamicTransform() == true) - { - return IdentifierName("dynamic"); - } - - var result = ArrayType(signature.BaseType.AcceptVisitor(this, context), [ArrayRankSpecifier()]); - return nullableAnnotation == NullableAnnotation.Annotated ? AddInformationTo(NullableType(result), signature) : result; - } - - public TypeSyntax VisitArrayType(ArrayTypeSignature signature, TypeContext context) - { - var nullableAnnotation = context.Transform.TryConsumeNullableTransform(); - if (context.Transform.TryConsumeDynamicTransform() == true) - { - return IdentifierName("dynamic"); - } - - var result = ArrayType(signature.BaseType.AcceptVisitor(this, context), SingletonList(ArrayRankSpecifier( - [.. Enumerable.Repeat(OmittedArraySizeExpression(), signature.Rank)] - ))); - return nullableAnnotation == NullableAnnotation.Annotated ? AddInformationTo(NullableType(result), signature) : result; - } - - public TypeSyntax VisitTypeDefOrRef(TypeDefOrRefSignature signature, TypeContext context) - { - return VisitTypeDefOrRef(signature.Type, context); - } - - public TypeSyntax VisitFunctionPointerType(FunctionPointerTypeSignature signature, TypeContext context) - { - FunctionPointerCallingConventionSyntax? callingConventionSyntax = null; - // For varargs there is no C# syntax. You get a use-site diagnostic if you attempt to use it, and just - // making a default-convention symbol is likely good enough. This is only observable through metadata - // that always be uncompilable in C# anyway. - if (signature.Signature.CallingConvention is not CallingConventionAttributes.Default and not CallingConventionAttributes.VarArg) - { - IEnumerable conventionsList = signature.Signature.CallingConvention switch - { - CallingConventionAttributes.C => [GetConventionForString("Cdecl")], - CallingConventionAttributes.StdCall => [GetConventionForString("Stdcall")], - CallingConventionAttributes.ThisCall => [GetConventionForString("Thiscall")], - CallingConventionAttributes.FastCall => [GetConventionForString("Fastcall")], - // CallingConventionAttributes.Unmanaged => - // // All types that come from CallingConventionTypes start with "CallConv". We don't want the prefix for the actual - // // syntax, so strip it off - // signature.Signature.UnmanagedCallingConventionTypes.IsEmpty - // ? null - // : symbol.Signature.UnmanagedCallingConventionTypes.Select(type => GetConventionForString(type.Name["CallConv".Length..])), - - _ => throw new Exception(), - }; - - callingConventionSyntax = FunctionPointerCallingConvention( - Token(SyntaxKind.UnmanagedKeyword), - conventionsList is object - ? FunctionPointerUnmanagedCallingConventionList([.. conventionsList]) - : null); - - static FunctionPointerUnmanagedCallingConventionSyntax GetConventionForString(string identifier) - => FunctionPointerUnmanagedCallingConvention(Identifier(identifier)); - } - - // var parameters = signature.Signature.ParameterTypes.Select(p => (p.Type, RefKindModifiers: CSharpSyntaxGeneratorInternal.GetParameterModifiers(p))) - // .Concat([ - // ( - // Type: signature.Signature.ReturnType, - // RefKindModifiers: CSharpSyntaxGeneratorInternal.GetParameterModifiers(isScoped: false, symbol.Signature.RefKind, isParams: false, forFunctionPointerReturnParameter: true)) - // ]) - // .SelectAsArray(t => FunctionPointerParameter(t.Type.GenerateTypeSyntax()).WithModifiers(t.RefKindModifiers)); - - var parameters = signature.Signature.ParameterTypes - .Append(signature.Signature.ReturnType) - .Select(t => FunctionPointerParameter(VisitTypeDescriptor(t, context))); - - return AddInformationTo(FunctionPointerType(callingConventionSyntax, FunctionPointerParameterList([.. parameters])), signature); - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/DeclarationModifiersExtensions.cs b/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/DeclarationModifiersExtensions.cs deleted file mode 100644 index 48c17e9..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/DeclarationModifiersExtensions.cs +++ /dev/null @@ -1,35 +0,0 @@ -using AsmResolver.DotNet; -using AsmResolver.DotNet.Signatures; -using CompatUnbreaker.Tool.Utilities.AsmResolver; -using CompatUnbreaker.Utilities.AsmResolver; -using Microsoft.CodeAnalysis.Editing; - -namespace CompatUnbreaker.Tool.SkeletonGeneration.CodeGeneration; - -internal static class DeclarationModifiersExtensions -{ - public static DeclarationModifiers GetModifiers(this IMemberDefinition member) - { - var field = member as FieldDefinition; - var property = member as PropertyDefinition; - var method = member as MethodDefinition; - var type = member as TypeDefinition; - var isConst = field?.IsLiteral == true || field?.Constant != null; - - return DeclarationModifiers.None - .WithIsStatic(member.IsStatic() && !isConst) - .WithIsAbstract(member.IsRoslynAbstract()) - .WithIsReadOnly(field?.IsInitOnly == true || property?.IsReadOnly() == true || type?.IsReadOnly == true || method?.IsReadOnly() == true) - .WithIsVirtual(member.IsRoslynVirtual()) - .WithIsOverride(member.IsOverride()) - .WithIsSealed(member.IsRoslynSealed()) - .WithIsConst(isConst) - // .WithIsUnsafe(symbol.RequiresUnsafeModifier()) TODO - .WithIsRef(field?.Signature?.FieldType is ByReferenceTypeSignature || type?.IsByRefLike == true) - .WithIsVolatile(field?.IsVolatile() == true) - .WithIsExtern(member.IsExtern()) - .WithAsync(method?.IsAsync() == true) - .WithIsRequired(member.IsRequired()) - .WithIsFile(type?.IsFileLocal() == true); - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/NullableAnnotation.cs b/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/NullableAnnotation.cs deleted file mode 100644 index 55d2b85..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/NullableAnnotation.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace CompatUnbreaker.Tool.SkeletonGeneration.CodeGeneration; - -internal enum NullableAnnotation : byte -{ - Oblivious = 0, - NotAnnotated = 1, - Annotated = 2, -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/RoslynIdentifierExtensions.cs b/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/RoslynIdentifierExtensions.cs deleted file mode 100644 index 3019ee4..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/RoslynIdentifierExtensions.cs +++ /dev/null @@ -1,57 +0,0 @@ -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Simplification; - -namespace CompatUnbreaker.Tool.SkeletonGeneration.CodeGeneration; - -internal static class RoslynIdentifierExtensions -{ - public static string EscapeIdentifier( - this string identifier, - bool isQueryContext = false - ) - { - var nullIndex = identifier.IndexOf('\0', StringComparison.Ordinal); - if (nullIndex >= 0) - { - identifier = identifier[..nullIndex]; - } - - var needsEscaping = SyntaxFacts.GetKeywordKind(identifier) != SyntaxKind.None; - - // Check if we need to escape this contextual keyword - needsEscaping = needsEscaping || (isQueryContext && SyntaxFacts.IsQueryContextualKeyword(SyntaxFacts.GetContextualKeywordKind(identifier))); - - return needsEscaping ? "@" + identifier : identifier; - } - - public static SyntaxToken ToIdentifierToken(this string identifier, bool isQueryContext = false) - { - var escaped = identifier.EscapeIdentifier(isQueryContext); - - if (escaped.Length == 0 || escaped[0] != '@') - { - return SyntaxFactory.Identifier(escaped); - } - - var unescaped = identifier.StartsWith('@') - ? identifier[1..] - : identifier; - - var token = SyntaxFactory.Identifier( - default, SyntaxKind.None, "@" + unescaped, unescaped, default); - - if (!identifier.StartsWith('@')) - { - token = token.WithAdditionalAnnotations(Simplifier.Annotation); - } - - return token; - } - - public static IdentifierNameSyntax ToIdentifierName(this string identifier) - { - return SyntaxFactory.IdentifierName(identifier.ToIdentifierToken()); - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Attributes.cs b/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Attributes.cs deleted file mode 100644 index 5ddfcd6..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Attributes.cs +++ /dev/null @@ -1,184 +0,0 @@ -using System.Diagnostics; -using AsmResolver; -using AsmResolver.DotNet; -using AsmResolver.DotNet.Signatures; -using AsmResolver.PE.DotNet.Metadata.Tables; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Editing; - -namespace CompatUnbreaker.Tool.SkeletonGeneration.CodeGeneration; - -internal static partial class SyntaxGeneratorExtensions -{ - public static SyntaxNode AddAttributes(this SyntaxGenerator syntaxGenerator, SyntaxNode declaration, IEnumerable attributes) - { - return syntaxGenerator.AddAttributes(declaration, syntaxGenerator.Attributes(attributes)); - } - - public static IEnumerable Attributes(this SyntaxGenerator syntaxGenerator, IEnumerable attributes) - { - return attributes.Where(a => !IsReserved(a)).Select(syntaxGenerator.Attribute); - } - - private static bool IsReserved(CustomAttribute attribute) - { - var type = attribute.Constructor?.DeclaringType; - if (type == null) return false; - - if (type.IsTypeOf("System", "ObsoleteAttribute")) - { - const string ByRefLikeMarker = "Types with embedded references are not supported in this version of your compiler."; - const string RequiredMembersMarker = "Constructors of types with required members are not supported in this version of your compiler."; - - return attribute.Signature?.FixedArguments.FirstOrDefault()?.Element is Utf8String { Value: ByRefLikeMarker or RequiredMembersMarker }; - } - - if (type.Namespace == "System.Runtime.CompilerServices") - { - switch (type.Name) - { - case "DynamicAttribute": - case "IsReadOnlyAttribute": - case "RequiresLocationAttribute": - case "IsUnmanagedAttribute": - case "IsByRefLikeAttribute": - case "CompilerFeatureRequiredAttribute": - case "TupleElementNamesAttribute": - case "NullableAttribute": - case "NullableContextAttribute": - case "NullablePublicOnlyAttribute": - // case "NativeIntegerAttribute": TODO roslyn doesn't emit this for me? huh? - case "ExtensionAttribute": - case "RequiredMemberAttribute": - case "ScopedRefAttribute": - case "RefSafetyRulesAttribute": - case "ParamCollectionAttribute": - return true; - } - } - else if (type.Namespace == "System") - { - switch (type.Name) - { - case "ParamArrayAttribute": - return true; - } - } - - return false; - } - - public static SyntaxNode Attribute(this SyntaxGenerator syntaxGenerator, CustomAttribute attribute) - { - ArgumentNullException.ThrowIfNull(attribute.Constructor); - ArgumentNullException.ThrowIfNull(attribute.Constructor.DeclaringType); - ArgumentNullException.ThrowIfNull(attribute.Constructor.Signature); - ArgumentNullException.ThrowIfNull(attribute.Signature); - - var args = attribute.Signature.FixedArguments.Select((a, i) => - { - var parameterType = attribute.Constructor.Signature.ParameterTypes[i]; - Debug.Assert(SignatureComparer.Default.Equals(parameterType, a.ArgumentType)); - return syntaxGenerator.AttributeArgument(syntaxGenerator.AttributeArgumentExpression(a)); - }) - .Concat(attribute.Signature.NamedArguments.Select(n => syntaxGenerator.AttributeArgument(n.MemberName, syntaxGenerator.AttributeArgumentExpression(n.Argument)))) - .ToArray(); - - return syntaxGenerator.Attribute( - name: syntaxGenerator.TypeExpression(attribute.Constructor.DeclaringType, TypeContext.Empty), - attributeArguments: args.Length > 0 ? args : null - ); - } - - private static SyntaxNode AttributeArgumentExpression(this SyntaxGenerator syntaxGenerator, CustomAttributeArgument argument) - { - var value = argument.ArgumentType.ElementType == ElementType.SzArray - ? (argument.IsNullArray ? null : argument.Elements) - : argument.Element; - - var typeContext = TypeContext.Empty; // CAs can't use generics - return syntaxGenerator.LiteralExpression(argument.ArgumentType, value, typeContext); - } - - public static SyntaxNode LiteralExpression(this SyntaxGenerator syntaxGenerator, TypeSignature typeSignature, object? value, TypeContext typeContext) - { - if (value == null) - { - if (!typeSignature.IsValueType) - { - return syntaxGenerator.CastExpression( - syntaxGenerator.NullableTypeExpression(syntaxGenerator.TypeExpression(typeSignature, typeContext)), - syntaxGenerator.NullLiteralExpression() - ); - } - - return syntaxGenerator.DefaultExpression(syntaxGenerator.TypeExpression(typeSignature, typeContext)); - } - - if (value is BoxedArgument boxedArgument) - { - // null objects get serialized as boxed null strings - if (typeSignature.ElementType == ElementType.Object && boxedArgument.Type.ElementType == ElementType.String && boxedArgument.Value == null) - { - return syntaxGenerator.LiteralExpression(typeSignature, null, typeContext); - } - - return syntaxGenerator.CastExpression( - syntaxGenerator.TypeExpression(typeSignature, typeContext), - syntaxGenerator.LiteralExpression(boxedArgument.Type, boxedArgument.Value, typeContext) - ); - } - - if (typeSignature.ElementType == ElementType.String) - { - return syntaxGenerator.LiteralExpression(((Utf8String) value).Value); - } - - if (typeSignature.ElementType.IsPrimitive()) - { - return syntaxGenerator.LiteralExpression(value); - } - - if (typeSignature is SzArrayTypeSignature arrayTypeSignature) - { - var baseType = arrayTypeSignature.BaseType; - var values = (IList) value; - - return syntaxGenerator.ArrayCreationExpression( - syntaxGenerator.TypeExpression(baseType, typeContext), - values.Select(o => syntaxGenerator.LiteralExpression(baseType, o, typeContext)) - ); - } - - if (typeSignature.IsTypeOf("System", "Type")) - { - var type = (ITypeDescriptor) value; - return syntaxGenerator.TypeOfExpression(syntaxGenerator.TypeExpression(type, typeContext)); - } - - if (typeSignature.Resolve() is { IsEnum: true } enumType) - { - return syntaxGenerator.CreateEnumConstantValue(enumType, value, typeContext); - } - - throw new NotSupportedException($"Couldn't generate a LiteralExpression for type '{typeSignature}' with value '{value}'"); - } - - private static bool IsPrimitive(this ElementType elementType) - { - switch (elementType) - { - case ElementType.Void: - case ElementType.Boolean: - case ElementType.Char: - case ElementType.I1 or ElementType.I2 or ElementType.I4 or ElementType.I8: - case ElementType.U1 or ElementType.U2 or ElementType.U4 or ElementType.U8: - case ElementType.R4 or ElementType.R8: - case ElementType.I or ElementType.U: - return true; - - default: - return false; - } - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.EnumValues.cs b/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.EnumValues.cs deleted file mode 100644 index 0d9c855..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.EnumValues.cs +++ /dev/null @@ -1,216 +0,0 @@ -using AsmResolver.DotNet; -using AsmResolver.PE.DotNet.Metadata.Tables; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Editing; - -namespace CompatUnbreaker.Tool.SkeletonGeneration.CodeGeneration; - -internal static partial class SyntaxGeneratorExtensions -{ - public static SyntaxNode CreateEnumConstantValue(this SyntaxGenerator syntaxGenerator, TypeDefinition enumType, object constantValue, TypeContext typeContext) - { - if (enumType.HasCustomAttribute("System", "FlagsAttribute")) - { - return syntaxGenerator.CreateFlagsEnumConstantValue(enumType, constantValue, typeContext); - } - - return syntaxGenerator.CreateNonFlagsEnumConstantValue(enumType, constantValue, typeContext); - } - - private static SyntaxNode CreateNonFlagsEnumConstantValue(this SyntaxGenerator syntaxGenerator, TypeDefinition enumType, object constantValue, TypeContext typeContext) - { - var underlyingType = enumType.GetEnumUnderlyingType(); - var constantValueULong = ConvertUnderlyingValueToUInt64(underlyingType.ElementType, constantValue); - - foreach (var field in enumType.Fields) - { - if (field is { Constant: not null }) - { - var fieldValue = ConvertUnderlyingValueToUInt64(underlyingType.ElementType, field.Constant.InterpretData()); - if (constantValueULong == fieldValue) - return syntaxGenerator.CreateMemberAccessExpression(field, enumType, underlyingType.ElementType, typeContext); - } - } - - // Otherwise, just add the enum as a literal. - return syntaxGenerator.CreateExplicitlyCastedLiteralValue(enumType, underlyingType.ElementType, constantValue, typeContext); - } - - private static SyntaxNode CreateMemberAccessExpression( - this SyntaxGenerator syntaxGenerator, - FieldDefinition field, - TypeDefinition enumType, - ElementType underlyingSpecialType, - TypeContext typeContext - ) - { - if (SyntaxFacts.IsValidIdentifier(field.Name)) - { - return syntaxGenerator.MemberAccessExpression(syntaxGenerator.TypeExpression(enumType, typeContext), syntaxGenerator.IdentifierName(field.Name)); - } - - return syntaxGenerator.CreateExplicitlyCastedLiteralValue(enumType, underlyingSpecialType, field.Constant.InterpretData(), typeContext); - } - - private static SyntaxNode CreateExplicitlyCastedLiteralValue( - this SyntaxGenerator syntaxGenerator, - TypeDefinition enumType, - ElementType underlyingSpecialType, - object constantValue, - TypeContext typeContext - ) - { - var expression = syntaxGenerator.LiteralExpression(constantValue); - - var constantValueULong = ConvertUnderlyingValueToUInt64(underlyingSpecialType, constantValue); - if (constantValueULong == 0) - { - // 0 is always convertible to an enum type without needing a cast. - return expression; - } - - return syntaxGenerator.CastExpression(syntaxGenerator.TypeExpression(enumType, typeContext), expression); - } - - private static ulong ConvertUnderlyingValueToUInt64(ElementType enumUnderlyingType, object value) - { - unchecked - { - return enumUnderlyingType switch - { - ElementType.I1 => (ulong) (sbyte) value, - ElementType.I2 => (ulong) (short) value, - ElementType.I4 => (ulong) (int) value, - ElementType.I8 => (ulong) (long) value, - ElementType.U1 => (byte) value, - ElementType.U2 => (ushort) value, - ElementType.U4 => (uint) value, - ElementType.U8 => (ulong) value, - _ => throw new ArgumentOutOfRangeException(nameof(enumUnderlyingType), enumUnderlyingType, null), - }; - } - } - - private static SyntaxNode CreateFlagsEnumConstantValue(this SyntaxGenerator syntaxGenerator, TypeDefinition enumType, object constantValue, TypeContext typeContext) - { - // These values are sorted by value. Don't change this. - var allFieldsAndValues = new List<(FieldDefinition field, ulong value)>(); - GetSortedEnumFieldsAndValues(enumType, allFieldsAndValues); - - var usedFieldsAndValues = new List<(FieldDefinition field, ulong value)>(); - return syntaxGenerator.CreateFlagsEnumConstantValue(enumType, constantValue, allFieldsAndValues, usedFieldsAndValues, typeContext); - } - - private static SyntaxNode CreateFlagsEnumConstantValue( - this SyntaxGenerator syntaxGenerator, - TypeDefinition enumType, - object constantValue, - List<(FieldDefinition field, ulong value)> allFieldsAndValues, - List<(FieldDefinition field, ulong value)> usedFieldsAndValues, - TypeContext typeContext - ) - { - var underlyingSpecialType = enumType.GetEnumUnderlyingType(); - var constantValueULong = ConvertUnderlyingValueToUInt64(underlyingSpecialType.ElementType, constantValue); - - var result = constantValueULong; - - // We will not optimize this code further to keep it maintainable. There are some - // boundary checks that can be applied to minimize the comparisons required. This code - // works the same for the best/worst case. In general the number of items in an enum are - // sufficiently small and not worth the optimization. - for (var index = allFieldsAndValues.Count - 1; index >= 0 && result != 0; index--) - { - var fieldAndValue = allFieldsAndValues[index]; - var valueAtIndex = fieldAndValue.value; - - if (valueAtIndex != 0 && (result & valueAtIndex) == valueAtIndex) - { - result -= valueAtIndex; - usedFieldsAndValues.Add(fieldAndValue); - } - } - - // We were able to represent this number as a bitwise OR of valid flags. - if (result == 0 && usedFieldsAndValues.Count > 0) - { - // We want to emit the fields in lower to higher value. So we walk backward. - SyntaxNode? finalNode = null; - for (var i = usedFieldsAndValues.Count - 1; i >= 0; i--) - { - var field = usedFieldsAndValues[i]; - var node = syntaxGenerator.CreateMemberAccessExpression(field.field, enumType, underlyingSpecialType.ElementType, typeContext); - if (finalNode == null) - { - finalNode = node; - } - else - { - finalNode = syntaxGenerator.BitwiseOrExpression(finalNode, node); - } - } - - return finalNode; - } - - // We couldn't find fields to OR together to make the value. - - // If we had 0 as the value, and there's an enum value equal to 0, then use that. - var zeroField = GetZeroField(allFieldsAndValues); - if (constantValueULong == 0 && zeroField != null) - { - return syntaxGenerator.CreateMemberAccessExpression(zeroField, enumType, underlyingSpecialType.ElementType, typeContext); - } - else - { - // Add anything else in as a literal value. - return syntaxGenerator.CreateExplicitlyCastedLiteralValue(enumType, underlyingSpecialType.ElementType, constantValue, typeContext); - } - } - - private static FieldDefinition? GetZeroField(List<(FieldDefinition field, ulong value)> allFieldsAndValues) - { - for (var i = allFieldsAndValues.Count - 1; i >= 0; i--) - { - var (field, value) = allFieldsAndValues[i]; - if (value == 0) - { - return field; - } - } - - return null; - } - - private static void GetSortedEnumFieldsAndValues( - TypeDefinition enumType, - List<(FieldDefinition field, ulong value)> allFieldsAndValues - ) - { - var underlyingType = enumType.GetEnumUnderlyingType(); - foreach (var field in enumType.Fields) - { - if (field is { Constant: not null }) - { - var value = ConvertUnderlyingValueToUInt64(underlyingType.ElementType, field.Constant.InterpretData()); - allFieldsAndValues.Add((field, value)); - } - } - - allFieldsAndValues.Sort(Compare); - - static int Compare((FieldDefinition field, ulong value) x, (FieldDefinition field, ulong value) y) - { - unchecked - { - return - (long) x.value < (long) y.value - ? -1 - : (long) x.value > (long) y.value - ? 1 - : -x.field.Name.CompareTo(y.field.Name); - } - } - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Events.cs b/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Events.cs deleted file mode 100644 index b168420..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Events.cs +++ /dev/null @@ -1,49 +0,0 @@ -using AsmResolver.DotNet; -using CompatUnbreaker.Tool.Utilities.AsmResolver; -using CompatUnbreaker.Utilities.AsmResolver; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Editing; -using Accessibility = Microsoft.CodeAnalysis.Accessibility; - -namespace CompatUnbreaker.Tool.SkeletonGeneration.CodeGeneration; - -internal static partial class SyntaxGeneratorExtensions -{ - public static SyntaxNode EventDeclaration(this SyntaxGenerator syntaxGenerator, EventDefinition @event) - { - ArgumentNullException.ThrowIfNull(@event.Name); - ArgumentNullException.ThrowIfNull(@event.EventType); - - var typeContext = TypeContext.From(@event, @event); - - var isAuto = @event.AddMethod?.IsCompilerGenerated() == true; - - SyntaxNode declaration; - - if (isAuto) - { - declaration = syntaxGenerator.EventDeclaration( - @event.Name, - syntaxGenerator.TypeExpression(@event.EventType, typeContext), - (Accessibility) @event.GetAccessibility(), - @event.GetModifiers() - ); - } - else - { - declaration = syntaxGenerator.CustomEventDeclaration( - @event.Name, - syntaxGenerator.TypeExpression(@event.EventType, typeContext), - (Accessibility) @event.GetAccessibility(), - @event.GetModifiers() - ); - } - - // TODO - // if (symbol.ExplicitInterfaceImplementations.Length > 0) - // { - // ev = this.WithExplicitInterfaceImplementations(ev, ImmutableArray.CastUp(symbol.ExplicitInterfaceImplementations)); - // } - return syntaxGenerator.AddAttributes(declaration, @event.CustomAttributes); - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Fields.cs b/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Fields.cs deleted file mode 100644 index fbd1b42..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Fields.cs +++ /dev/null @@ -1,39 +0,0 @@ -using AsmResolver.DotNet; -using CompatUnbreaker.Utilities.AsmResolver; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Editing; -using Accessibility = Microsoft.CodeAnalysis.Accessibility; - -namespace CompatUnbreaker.Tool.SkeletonGeneration.CodeGeneration; - -internal static partial class SyntaxGeneratorExtensions -{ - public static SyntaxNode FieldDeclaration(this SyntaxGenerator syntaxGenerator, FieldDefinition field) - { - var typeContext = TypeContext.From(field, field); - var initializer = field.Constant is { } constant - ? syntaxGenerator.LiteralExpression(field.DeclaringType?.IsEnum == true ? field.DeclaringType.GetEnumUnderlyingType() : field.Signature.FieldType, constant.InterpretData(), typeContext) - : null; - - return syntaxGenerator.FieldDeclaration(field, initializer); - } - - public static SyntaxNode FieldDeclaration(this SyntaxGenerator syntaxGenerator, FieldDefinition field, SyntaxNode? initializer) - { - ArgumentNullException.ThrowIfNull(field.Name); - ArgumentNullException.ThrowIfNull(field.Signature); - - var typeContext = TypeContext.From(field, field); - - return syntaxGenerator.AddAttributes( - syntaxGenerator.FieldDeclaration( - field.Name, - syntaxGenerator.TypeExpression(field.Signature.FieldType, typeContext), - (Accessibility) field.GetAccessibility(), - field.GetModifiers(), - initializer - ), - field.CustomAttributes - ); - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Generics.cs b/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Generics.cs deleted file mode 100644 index 9a9aea1..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Generics.cs +++ /dev/null @@ -1,132 +0,0 @@ -using AsmResolver.DotNet; -using AsmResolver.PE.DotNet.Metadata.Tables; -using CompatUnbreaker.Tool.Utilities.AsmResolver; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Editing; -using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; - -namespace CompatUnbreaker.Tool.SkeletonGeneration.CodeGeneration; - -internal static partial class SyntaxGeneratorExtensions -{ - public static SyntaxNode WithTypeParametersAndConstraints(this SyntaxGenerator syntaxGenerator, SyntaxNode declaration, IList typeParameters, TypeContext typeContext) - { - if (typeParameters.Count <= 0) - return declaration; - - declaration = WithTypeParameters(syntaxGenerator, declaration, typeParameters.Select(syntaxGenerator.TypeParameter)); - - foreach (var typeParameter in typeParameters) - { - ArgumentNullException.ThrowIfNull(typeParameter.Name); - - var parameterContext = typeContext.WithTransformsAttributeProvider(typeParameter); - var hasNotNullConstraint = parameterContext.Transform.TryConsumeNullableTransform() == NullableAnnotation.NotAnnotated; - - if (HasSomeConstraint(typeParameter) || hasNotNullConstraint) - { - var constraints = typeParameter.Constraints - .Where(c => - c.Constraint != null && - (!typeParameter.HasNotNullableValueTypeConstraint || !c.Constraint.ToTypeSignature().StripModifiers().IsTypeOf("System", "ValueType")) - ); - - var kinds = SpecialTypeConstraintKind.None; - if (typeParameter.HasDefaultConstructorConstraint && !typeParameter.HasNotNullableValueTypeConstraint) - kinds |= SpecialTypeConstraintKind.Constructor; - if (typeParameter.HasReferenceTypeConstraint && hasNotNullConstraint) - kinds |= SpecialTypeConstraintKind.ReferenceType; - if (typeParameter.HasNotNullableValueTypeConstraint) - kinds |= SpecialTypeConstraintKind.ValueType; - - declaration = syntaxGenerator.WithTypeConstraint( - declaration, - typeParameter.Name, - kinds, - isUnamangedType: typeParameter.HasUnmanagedTypeConstraint(), - types: constraints.Select(c => syntaxGenerator.TypeExpression(c.Constraint!, typeContext)) - ); - - if (hasNotNullConstraint) - { - if (!typeParameter.HasReferenceTypeConstraint) - { - declaration = AddTypeConstraints( - declaration, - typeParameter.Name, - [TypeConstraint(IdentifierName("notnull"))], - true - ); - } - } - else if (typeParameter.HasReferenceTypeConstraint) - { - declaration = AddTypeConstraints( - declaration, - typeParameter.Name, - [ClassOrStructConstraint(SyntaxKind.ClassConstraint).WithQuestionToken(Token(SyntaxKind.QuestionToken))], - true - ); - } - - if (typeParameter.HasAllowByRefLike) - { - declaration = AddTypeConstraints( - declaration, - typeParameter.Name, - [AllowsConstraintClause([RefStructConstraint()])] - ); - } - } - } - - return declaration; - } - - private static bool HasSomeConstraint(GenericParameter typeParameter) - { - return typeParameter.HasDefaultConstructorConstraint || - typeParameter.HasReferenceTypeConstraint || - typeParameter.HasNotNullableValueTypeConstraint || - typeParameter.Constraints.Count > 0; - } - - private static SyntaxNode TypeParameter(this SyntaxGenerator syntaxGenerator, GenericParameter typeParameter) - { - return SyntaxFactory.TypeParameter( - attributeLists: [.. syntaxGenerator.Attributes(typeParameter.CustomAttributes).Cast()], - varianceKeyword: typeParameter.Variance switch - { - GenericParameterAttributes.Contravariant => Token(SyntaxKind.InKeyword), - GenericParameterAttributes.Covariant => Token(SyntaxKind.OutKeyword), - _ => default, - }, - Identifier(typeParameter.Name!) - ); - } - - private static SyntaxNode AddTypeConstraints(SyntaxNode declaration, string typeParameterName, SeparatedSyntaxList constraints, bool insert = false) - { - return declaration switch - { - MethodDeclarationSyntax method => method.WithConstraintClauses(AddTypeConstraints(method.ConstraintClauses, typeParameterName, constraints, insert)), - TypeDeclarationSyntax type => type.WithConstraintClauses(AddTypeConstraints(type.ConstraintClauses, typeParameterName, constraints, insert)), - DelegateDeclarationSyntax @delegate => @delegate.WithConstraintClauses(AddTypeConstraints(@delegate.ConstraintClauses, typeParameterName, constraints, insert)), - _ => declaration, - }; - } - - private static SyntaxList AddTypeConstraints(SyntaxList clauses, string typeParameterName, SeparatedSyntaxList constraints, bool insert) - { - var clause = clauses.FirstOrDefault(c => c.Name.Identifier.ToString() == typeParameterName); - - if (clause == null) - { - return clauses.Add(TypeParameterConstraintClause(typeParameterName.ToIdentifierName(), constraints)); - } - - return clauses.Replace(clause, clause.WithConstraints(insert ? clause.Constraints.InsertRange(0, constraints) : clause.Constraints.AddRange(constraints))); - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Methods.cs b/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Methods.cs deleted file mode 100644 index 0ce64e7..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Methods.cs +++ /dev/null @@ -1,428 +0,0 @@ -using System.Runtime.InteropServices; -using AsmResolver.DotNet; -using AsmResolver.DotNet.Collections; -using AsmResolver.DotNet.Signatures; -using AsmResolver.PE.DotNet.Metadata.Tables; -using CompatUnbreaker.Tool.Utilities.AsmResolver; -using CompatUnbreaker.Utilities.AsmResolver; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Editing; -using Accessibility = Microsoft.CodeAnalysis.Accessibility; - -namespace CompatUnbreaker.Tool.SkeletonGeneration.CodeGeneration; - -internal static partial class SyntaxGeneratorExtensions -{ - // private static bool IsValidStaticUserDefinedOperatorSignature(this MethodDefinition method, int parameterCount) - // { - // if (method.Signature.ReturnType.ElementType == ElementType.Void || - // method.GenericParameters.Count > 0 || - // method.Signature.CallingConvention == CallingConventionAttributes.VarArg || - // method.Parameters.Count != parameterCount || - // method.IsParams()) - // { - // return false; - // } - // - // return method.HasValidOperatorParameterRefKinds(); - // } - // - // private static bool HasValidOperatorParameterRefKinds(this MethodDefinition method) - // { - // if (this.ParameterRefKinds.IsDefault) - // { - // return true; - // } - // - // foreach (var kind in this.ParameterRefKinds) - // { - // switch (kind) - // { - // case RefKind.None: - // case RefKind.In: - // continue; - // case RefKind.Out: - // case RefKind.Ref: - // case RefKind.RefReadOnlyParameter: - // return false; - // default: - // throw ExceptionUtilities.UnexpectedValue(kind); - // } - // } - // - // return true; - // } - - public static SyntaxNode MethodDeclaration(this SyntaxGenerator syntaxGenerator, MethodDefinition method, IEnumerable? statements = null) - { - if (method.IsConstructor) - { - return syntaxGenerator.ConstructorDeclaration(method); - } - - if (method.IsDestructor()) - { - throw new NotImplementedException(); - } - - if (method.IsSpecialName && SyntaxFacts.GetOperatorKind(method.Name) != SyntaxKind.None) - { - var returnTypeContext2 = TypeContext.From(method, method.Parameters.ReturnParameter.GetOrCreateDefinition()); - var decl2 = syntaxGenerator.OperatorDeclaration( - method.Name, - isImplicitConversion: method.Name.Value is WellKnownMemberNames.ImplicitConversionName, - parameters: method.Parameters.Select(p => syntaxGenerator.ParameterDeclaration(p)), - returnType: method.Signature.ReturnType.ElementType == ElementType.Void ? null : TypeExpression(syntaxGenerator, method.Signature.ReturnType, method.Parameters.ReturnParameter.GetRefKind(), returnTypeContext2), - accessibility: (Accessibility) method.GetAccessibility(), - modifiers: method.GetModifiers(), - statements: statements - ); - - return decl2; - } - - // if (method.MethodKind == MethodKind.Destructor) - // { - // return syntaxGenerator.DestructorDeclaration(method); - // } - // - // if (method.MethodKind is MethodKind.UserDefinedOperator or MethodKind.Conversion) - // { - // return syntaxGenerator.OperatorDeclaration(method); - // } - - var typeContext = TypeContext.From(method, method); - - var returnTypeContext = TypeContext.From(method, method.Parameters.ReturnParameter.GetOrCreateDefinition()); - - var decl = MethodDeclaration( - syntaxGenerator, - method.Name, - typeParameters: method.GenericParameters.Select(syntaxGenerator.TypeParameter), - parameters: method.Parameters.Select(p => syntaxGenerator.ParameterDeclaration(p)), - returnType: method.Signature.ReturnType.ElementType == ElementType.Void ? null : TypeExpression(syntaxGenerator, method.Signature.ReturnType, method.Parameters.ReturnParameter.GetRefKind(), returnTypeContext), - accessibility: (Accessibility) method.GetAccessibility(), - modifiers: method.GetModifiers(), - statements: statements - ); - - if (method.GenericParameters.Count > 0) - { - // // Overrides are special. Specifically, in an override, if a type parameter has no constraints, then we - // // want to still add `where T : default` if that type parameter is used with NRT (e.g. `T?`) that way - // // the language can distinguish if this is a Nullable Value Type or not. - // if (method.IsOverride()) - // { - // foreach (var typeParameter in method.GenericParameters) - // { - // if (HasNullableAnnotation(typeParameter, method)) - // { - // if (!HasSomeConstraint(typeParameter)) - // { - // // if there are no constraints, add `where T : default` so it's known this not an NVT - // // and is just an unconstrained type parameter. - // decl = WithDefaultConstraint(decl, typeParameter.Name); - // } - // else if (!typeParameter.HasValueTypeConstraint) - // { - // // if there are some constraints, add `where T : class` so it's known this is not an NVT - // // and must specifically be some reference type. - // decl = WithTypeConstraint(decl, typeParameter.Name, SpecialTypeConstraintKind.ReferenceType); - // } - // } - // } - // } - // else - { - decl = syntaxGenerator.WithTypeParametersAndConstraints(decl, method.GenericParameters, typeContext); - } - } - - // if (method.ExplicitInterfaceImplementations.Length > 0) - // { - // decl = this.WithExplicitInterfaceImplementations(decl, - // ImmutableArray.CastUp(method.ExplicitInterfaceImplementations)); - // } - - if (method.IsPInvokeImpl) - { - var implementationMap = method.ImplementationMap; - - var dllName = implementationMap.Scope.Name.Value; - - var arguments = new List - { - syntaxGenerator.AttributeArgument(syntaxGenerator.LiteralExpression(dllName)), - }; - - if (implementationMap.Name != method.Name) - { - arguments.Add(syntaxGenerator.AttributeArgument( - "EntryPoint", - syntaxGenerator.LiteralExpression(implementationMap.Name.Value) - )); - } - - var charSet = implementationMap.Attributes & ImplementationMapAttributes.CharSetMask; - if (charSet != ImplementationMapAttributes.CharSetNotSpec) - { - arguments.Add(syntaxGenerator.AttributeArgument( - "CharSet", - syntaxGenerator.MemberAccessExpression(SyntaxFactory.ParseTypeName("System.Runtime.InteropServices.CharSet"), charSet switch - { - ImplementationMapAttributes.CharSetAnsi => nameof(CharSet.Ansi), - ImplementationMapAttributes.CharSetUnicode => nameof(CharSet.Unicode), - ImplementationMapAttributes.CharSetAuto => nameof(CharSet.Auto), - _ => throw new ArgumentOutOfRangeException(), - }) - )); - } - - if ((implementationMap.Attributes & ImplementationMapAttributes.SupportsLastError) != 0) - { - arguments.Add(syntaxGenerator.AttributeArgument("SetLastError", syntaxGenerator.TrueLiteralExpression())); - } - - if ((implementationMap.Attributes & ImplementationMapAttributes.NoMangle) != 0) - { - arguments.Add(syntaxGenerator.AttributeArgument("ExactSpelling", syntaxGenerator.TrueLiteralExpression())); - } - - var callingConvention = implementationMap.Attributes & ImplementationMapAttributes.CallConvMask; - if (callingConvention != ImplementationMapAttributes.CallConvWinapi) - { - arguments.Add(syntaxGenerator.AttributeArgument( - "CallingConvention", - syntaxGenerator.MemberAccessExpression(SyntaxFactory.ParseTypeName("System.Runtime.InteropServices.CallingConvention"), callingConvention switch - { - ImplementationMapAttributes.CallConvCdecl => nameof(CallingConvention.Cdecl), - ImplementationMapAttributes.CallConvStdcall => nameof(CallingConvention.StdCall), - ImplementationMapAttributes.CallConvThiscall => nameof(CallingConvention.ThisCall), - ImplementationMapAttributes.CallConvFastcall => nameof(CallingConvention.FastCall), - _ => throw new ArgumentOutOfRangeException(), - }) - )); - } - - - var bestFitMapping = implementationMap.Attributes & ImplementationMapAttributes.BestFitMask; - if (bestFitMapping != ImplementationMapAttributes.BestFitUseAssem) - { - arguments.Add(syntaxGenerator.AttributeArgument( - "BestFitMapping", - bestFitMapping switch - { - ImplementationMapAttributes.BestFitEnabled => syntaxGenerator.TrueLiteralExpression(), - ImplementationMapAttributes.BestFitDisabled => syntaxGenerator.FalseLiteralExpression(), - _ => throw new ArgumentOutOfRangeException(), - }) - ); - } - - if (!method.PreserveSignature) - { - arguments.Add(syntaxGenerator.AttributeArgument("PreserveSig", syntaxGenerator.FalseLiteralExpression())); - } - - var throwOnUnmappableChar = implementationMap.Attributes & ImplementationMapAttributes.ThrowOnUnmappableCharMask; - if (throwOnUnmappableChar != ImplementationMapAttributes.ThrowOnUnmappableCharUseAssem) - { - arguments.Add(syntaxGenerator.AttributeArgument( - "ThrowOnUnmappableChar", - throwOnUnmappableChar switch - { - ImplementationMapAttributes.ThrowOnUnmappableCharEnabled => syntaxGenerator.TrueLiteralExpression(), - ImplementationMapAttributes.ThrowOnUnmappableCharDisabled => syntaxGenerator.FalseLiteralExpression(), - _ => throw new ArgumentOutOfRangeException(), - }) - ); - } - - decl = syntaxGenerator.AddAttributes(decl, syntaxGenerator.Attribute( - "System.Runtime.InteropServices.DllImportAttribute", - arguments - )); - } - - if (method.IsExtern()) - { - decl = ((BaseMethodDeclarationSyntax) decl) - .WithBody(null) - .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken).WithTrailingTrivia(SyntaxFactory.LineFeed)) - .WithLeadingTrivia(SyntaxFactory.LineFeed); - } - - var attributes = syntaxGenerator.Attributes(method.CustomAttributes) - .Concat( - syntaxGenerator.Attributes(method.Parameters.ReturnParameter.GetOrCreateDefinition().CustomAttributes) - .Cast() - .Select(a => a.WithTarget(SyntaxFactory.AttributeTargetSpecifier(SyntaxFactory.Token(SyntaxKind.ReturnKeyword)))) - ); - - return syntaxGenerator.AddAttributes(decl, attributes); - - // bool HasNullableAnnotation(ITypeParameterSymbol typeParameter, IMethodSymbol method) - // { - // return method.ReturnType.GetReferencedTypeParameters().Any(t => IsNullableAnnotatedTypeParameter(typeParameter, t)) || - // method.Parameters.Any(p => p.Type.GetReferencedTypeParameters().Any(t => IsNullableAnnotatedTypeParameter(typeParameter, t))); - // } - // - // static bool IsNullableAnnotatedTypeParameter(ITypeParameterSymbol typeParameter, ITypeParameterSymbol current) - // { - // return Equals(current, typeParameter) && current.NullableAnnotation == NullableAnnotation.Annotated; - // } - } - - private static SyntaxNode ConstructorDeclaration( - this SyntaxGenerator syntaxGenerator, - MethodDefinition constructorMethod, - IEnumerable? baseConstructorArguments = null, - IEnumerable? statements = null - ) - { - return syntaxGenerator.AddAttributes( - syntaxGenerator.ConstructorDeclaration( - constructorMethod.DeclaringType != null ? constructorMethod.DeclaringType.Name : "New", - constructorMethod.Parameters.Select(p => syntaxGenerator.ParameterDeclaration(p)), - (Accessibility) constructorMethod.GetAccessibility(), - constructorMethod.GetModifiers(), - baseConstructorArguments, - statements - ), - constructorMethod.CustomAttributes - ); - } - - public static SyntaxNode ParameterDeclaration(this SyntaxGenerator syntaxGenerator, Parameter symbol, SyntaxNode? initializer = null) - { - var definition = symbol.GetOrCreateDefinition(); - - var refKind = symbol.GetRefKind(); - - var typeContext = TypeContext.From(definition.Method, definition); - var type = syntaxGenerator.TypeExpression(symbol.ParameterType, typeContext); - if (type is RefTypeSyntax refType) - { - type = refType.Type; - } - - var isParams = definition.IsParams(); - - return syntaxGenerator.AddAttributes( - syntaxGenerator.ParameterDeclaration( - symbol.Name, - type, - initializer ?? (definition.Constant is { } constant - ? syntaxGenerator.LiteralExpression(symbol.ParameterType, constant.InterpretData(), typeContext) - : null), - refKind, - isExtension: symbol.Index == 0 && definition.Method?.IsExtension() == true, - isParams, - isScoped: symbol.IsScoped(refKind) && !isParams - ), - definition.CustomAttributes - ); - } - - private static RefKind GetRefKind(this Parameter parameter) - { - if (parameter.ParameterType.StripModifiers() is ByReferenceTypeSignature) - { - var definition = parameter.GetOrCreateDefinition(); - ArgumentNullException.ThrowIfNull(definition.Method); - - var isReturn = parameter == definition.Method.Parameters.ReturnParameter; - - if (definition.IsOut) - { - return RefKind.Out; - } - - if (!isReturn && definition.HasCustomAttribute("System.Runtime.CompilerServices", "RequiresLocationAttribute")) - { - return RefKind.RefReadOnlyParameter; - } - - if (definition.HasIsReadOnlyAttribute()) - { - return RefKind.In; - } - - return RefKind.Ref; - } - - return RefKind.None; - } - - private static bool IsScoped(this Parameter parameter, RefKind refKind) - { - var scopedKind = parameter.GetScopedKind(refKind); - - if (refKind is RefKind.Ref or RefKind.In or RefKind.RefReadOnlyParameter && scopedKind == ScopedKind.ScopedRef) - return true; - - bool isByRefLike; - - if (parameter.ParameterType is ByReferenceTypeSignature) - { - isByRefLike = true; - } - else if (parameter.ParameterType is GenericParameterSignature genericParameterSignature) - { - var method = parameter.GetOrCreateDefinition().Method; - - var genericParameters = genericParameterSignature.ParameterType switch - { - GenericParameterType.Type => method.DeclaringType.GenericParameters, - GenericParameterType.Method => method.GenericParameters, - _ => throw new ArgumentOutOfRangeException(), - }; - - var genericParameter = genericParameters[genericParameterSignature.Index]; - - isByRefLike = genericParameter.HasAllowByRefLike; - } - else if (parameter.ParameterType is TypeDefOrRefSignature or CorLibTypeSignature or GenericInstanceTypeSignature) - { - var parameterType = parameter.ParameterType.Resolve() - ?? throw new InvalidOperationException($"Couldn't resolve parameter type '{parameter.ParameterType}'"); - - isByRefLike = parameterType.IsByRefLike; - } - else - { - isByRefLike = false; - } - - return refKind is RefKind.None && isByRefLike && scopedKind == ScopedKind.ScopedValue; - } - - private static ScopedKind GetScopedKind(this Parameter parameter, RefKind refKind) - { - var definition = parameter.GetOrCreateDefinition(); - - if (definition.HasCustomAttribute("System.Diagnostics.CodeAnalysis", "UnscopedRefAttribute")) - { - return ScopedKind.None; - } - - if (definition.HasCustomAttribute("System.Runtime.CompilerServices", "ScopedRefAttribute")) - { - if (parameter.ParameterType.StripModifiers() is ByReferenceTypeSignature) - { - return ScopedKind.ScopedRef; - } - - return ScopedKind.ScopedValue; - } - - if ( /* TODO module.UseUpdatedEscapeRules && */ refKind == RefKind.Out) - { - return ScopedKind.ScopedRef; - } - - return ScopedKind.None; - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Properties.cs b/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Properties.cs deleted file mode 100644 index fbce653..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Properties.cs +++ /dev/null @@ -1,86 +0,0 @@ -using AsmResolver.DotNet; -using AsmResolver.DotNet.Signatures; -using CompatUnbreaker.Tool.Utilities.AsmResolver; -using CompatUnbreaker.Utilities.AsmResolver; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Editing; -using Accessibility = Microsoft.CodeAnalysis.Accessibility; - -namespace CompatUnbreaker.Tool.SkeletonGeneration.CodeGeneration; - -internal static partial class SyntaxGeneratorExtensions -{ - public static SyntaxNode PropertyDeclaration( - this SyntaxGenerator syntaxGenerator, - PropertyDefinition property, - IEnumerable? getAccessorStatements = null, - IEnumerable? setAccessorStatements = null - ) - { - /*property.IsIndexer ? syntaxGenerator.IndexerDeclaration(property) :*/ - - var typeContext = TypeContext.From(property, property); - - var propertyAccessibility = (Accessibility) property.GetAccessibility(); - var getMethodSymbol = property.GetMethod; - var setMethodSymbol = property.SetMethod; - - SyntaxNode? getAccessor = null; - SyntaxNode? setAccessor = null; - - if (getMethodSymbol is not null) - { - var getMethodAccessibility = (Accessibility) getMethodSymbol.GetAccessibility(); - getAccessor = syntaxGenerator.GetAccessorDeclaration(getMethodAccessibility < propertyAccessibility ? getMethodAccessibility : Accessibility.NotApplicable, getAccessorStatements); - } - - if (setMethodSymbol is not null) - { - var setMethodAccessibility = (Accessibility) setMethodSymbol.GetAccessibility(); - setAccessor = SetAccessorDeclaration( - syntaxGenerator, - setMethodAccessibility < propertyAccessibility ? setMethodAccessibility : Accessibility.NotApplicable, - isInitOnly: setMethodSymbol.IsInitOnly(), - setAccessorStatements); - } - - var propDecl = PropertyDeclaration( - syntaxGenerator, - property.Name, - TypeExpression(syntaxGenerator, property.Signature.ReturnType, property.GetRefKind(), typeContext), - getAccessor, - setAccessor, - propertyAccessibility, - property.GetModifiers()); - - // TODO - // if (property.ExplicitInterfaceImplementations.Length > 0) - // { - // propDecl = this.WithExplicitInterfaceImplementations(propDecl, - // ImmutableArray.CastUp(property.ExplicitInterfaceImplementations)); - // } - - return syntaxGenerator.AddAttributes(propDecl, property.CustomAttributes); - } - - private static bool IsInitOnly(this MethodDefinition method) - { - return !method.IsStatic /*&& method.PropertySet */ && - method.Signature.ReturnType?.HasCustomModifier(m => m.IsRequired && m.IsTypeOf("System.Runtime.CompilerServices", "IsExternalInit")) == true; - } - - private static RefKind GetRefKind(this PropertyDefinition property) - { - if (property.Signature.ReturnType is ByReferenceTypeSignature) - { - if (property.HasIsReadOnlyAttribute()) - { - return RefKind.RefReadOnly; - } - - return RefKind.Ref; - } - - return RefKind.None; - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Types.cs b/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Types.cs deleted file mode 100644 index f50b182..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.Types.cs +++ /dev/null @@ -1,209 +0,0 @@ -using AsmResolver.DotNet; -using AsmResolver.DotNet.Signatures; -using AsmResolver.PE.DotNet.Cil; -using AsmResolver.PE.DotNet.Metadata.Tables; -using CompatUnbreaker.Tool.Utilities; -using CompatUnbreaker.Tool.Utilities.AsmResolver; -using CompatUnbreaker.Utilities.AsmResolver; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Editing; -using Accessibility = Microsoft.CodeAnalysis.Accessibility; - -namespace CompatUnbreaker.Tool.SkeletonGeneration.CodeGeneration; - -internal static partial class SyntaxGeneratorExtensions -{ - public static SyntaxNode TypeDeclaration(this SyntaxGenerator syntaxGenerator, TypeDefinition type, Func? memberFilter = null) - { - ArgumentNullException.ThrowIfNull(type.Name); - - var name = MetadataHelpers.InferTypeArityAndUnmangleMetadataName(type.Name.Value, out var arity).ToString(); - - var typeContext = TypeContext.From(type, type); - - var interfaces = type.Interfaces.Select(i => - { - ArgumentNullException.ThrowIfNull(i.Interface); - return syntaxGenerator.TypeExpression(i.Interface, typeContext); - }); - - var members = type.GetMembers().Where(syntaxGenerator.CanBeDeclared); - if (memberFilter != null) members = members.Where(memberFilter); - - SyntaxNode declaration; - - if (type.IsDelegate) - { - var invoke = type.Methods.Single(m => m.Name == "Invoke"); - ArgumentNullException.ThrowIfNull(invoke.Signature); - - typeContext = TypeContext.From(invoke, invoke); - var returnType = invoke.Signature.ReturnType; - - declaration = syntaxGenerator.DelegateDeclaration( - name, - typeParameters: null, - parameters: invoke.Parameters.Select(p => syntaxGenerator.ParameterDeclaration(p)), - returnType: returnType.ElementType == ElementType.Void ? null : syntaxGenerator.TypeExpression(returnType, typeContext), - accessibility: (Accessibility) type.GetAccessibility(), - modifiers: type.GetModifiers() - ); - } - else if (type.IsEnum) - { - var underlyingType = type.GetEnumUnderlyingType(); - - declaration = syntaxGenerator.EnumDeclaration( - name, - underlyingType: underlyingType is null or { ElementType: ElementType.I4 } - ? null - : syntaxGenerator.TypeExpression(underlyingType, typeContext), - accessibility: (Accessibility) type.GetAccessibility(), - members: type.Fields.Where(f => f.Name != "value__").Select(m => syntaxGenerator.Declaration(m, memberFilter)) - ); - } - else if (type.IsValueType) - { - declaration = syntaxGenerator.StructDeclaration( - type.IsRecord(), - name, - typeParameters: null, - accessibility: (Accessibility) type.GetAccessibility(), - modifiers: type.GetModifiers(), - interfaceTypes: interfaces, - members: members.Select(m => syntaxGenerator.Declaration(m, memberFilter)) - ); - } - else if (type.IsInterface) - { - declaration = syntaxGenerator.InterfaceDeclaration( - name, - typeParameters: null, - accessibility: (Accessibility) type.GetAccessibility(), - interfaceTypes: interfaces, - members: members.Select(m => syntaxGenerator.Declaration(m, memberFilter)) - ); - } - else if (type.IsClass) - { - if (type.GetSynthesizedConstructor() is { } synthesizedConstructor) - { - members = members.Where(m => m != synthesizedConstructor); - } - - declaration = syntaxGenerator.ClassDeclaration( - type.IsRecord(), - name, - typeParameters: null, - accessibility: (Accessibility) type.GetAccessibility(), - modifiers: type.GetModifiers(), - baseType: type.BaseType != null && type.BaseType.ToTypeSignature().ElementType != ElementType.Object ? syntaxGenerator.TypeExpression(type.BaseType, typeContext) : null, - interfaceTypes: interfaces, - members: members.Select(m => syntaxGenerator.Declaration(m, memberFilter)).Select(d => d.WithLeadingTrivia(SyntaxFactory.LineFeed)) - ); - } - else - { - throw new ArgumentOutOfRangeException(nameof(type)); - } - - declaration = syntaxGenerator.WithTypeParametersAndConstraints(declaration, type.GenericParameters.TakeLast(arity).ToArray(), typeContext); - declaration = syntaxGenerator.AddAttributes(declaration, type.CustomAttributes); - - if (declaration is TypeDeclarationSyntax { Members.Count: 0 } or EnumDeclarationSyntax { Members.Count: 0 }) - { - declaration = ((BaseTypeDeclarationSyntax) declaration) - .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)) - .WithOpenBraceToken(default) - .WithCloseBraceToken(default); - } - - return declaration; - } - - public static bool CanBeDeclared(this SyntaxGenerator syntaxGenerator, IMemberDefinition member) - { - ArgumentNullException.ThrowIfNull(member.Name); - - if (member is IHasCustomAttribute hasCustomAttribute && hasCustomAttribute.IsCompilerGenerated()) - { - return false; - } - - if (member is MethodDefinition method) - { - if (method.IsConstructor || method.IsDestructor()) - { - return true; - } - - if (method.IsPropertyAccessor() || method.IsEventAccessor()) - { - return false; - } - } - - var name = MetadataHelpers.InferTypeArityAndUnmangleMetadataName(member.Name, out _).ToString(); - return SyntaxFacts.IsValidIdentifier(name); - } - - private static MethodDefinition? GetSynthesizedConstructor(this TypeDefinition type) - { - if (type.IsStatic() || type.IsInterface) - return null; - - var isRecord = type.IsRecord(); - - MethodDefinition? constructor = null; - - foreach (var method in type.Methods) - { - if (method is not { IsStatic: false, IsConstructor: true }) - continue; - - // Ignore the record copy constructor - if (isRecord && - method.Parameters.Count == 1 && - method.GenericParameters.Count == 0 && - SignatureComparer.Default.Equals(method.Parameters[0].ParameterType, type.ToTypeSignature())) - { - continue; - } - - if (constructor != null) - { - return null; - } - - constructor = method; - } - - if (constructor == null) - return null; - - if (constructor.Parameters.Count != 0 || - (constructor.Attributes & MethodAttributes.MemberAccessMask) != (type.IsAbstract ? MethodAttributes.Family : MethodAttributes.Public) || - constructor.CustomAttributes.Any(a => !IsReserved(a))) - { - return null; - } - - if (constructor.CilMethodBody is not { } methodBody || - !methodBody.Instructions.Match( - i => i.OpCode == CilOpCodes.Ldarg_0, - i => i.OpCode == CilOpCodes.Call && - i.Operand is IMethodDescriptor method && - method.DeclaringType == type.BaseType && - method.Name == constructor.Name && - method.Signature is { HasThis: true, ParameterTypes.Count: 0 }, - i => i.OpCode == CilOpCodes.Ret - )) - { - return null; - } - - return constructor; - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.UnsafeAccessors.cs b/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.UnsafeAccessors.cs deleted file mode 100644 index 1c94856..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.UnsafeAccessors.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System.Runtime.CompilerServices; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Editing; - -namespace CompatUnbreaker.Tool.SkeletonGeneration.CodeGeneration; - -internal static partial class SyntaxGeneratorExtensions -{ - [UnsafeAccessor(UnsafeAccessorKind.Method)] - private static extern SyntaxNode ClassDeclaration( - this SyntaxGenerator @this, - bool isRecord, - string name, - IEnumerable? typeParameters, - Accessibility accessibility, - DeclarationModifiers modifiers, - SyntaxNode? baseType, - IEnumerable? interfaceTypes, - IEnumerable? members - ); - - [UnsafeAccessor(UnsafeAccessorKind.Method)] - private static extern SyntaxNode StructDeclaration( - this SyntaxGenerator @this, - bool isRecord, - string name, - IEnumerable? typeParameters, - Accessibility accessibility, - DeclarationModifiers modifiers, - IEnumerable? interfaceTypes, - IEnumerable? members - ); - - [UnsafeAccessor(UnsafeAccessorKind.Method)] - private static extern SyntaxNode EnumDeclaration( - this SyntaxGenerator @this, - string name, - SyntaxNode? underlyingType, - Accessibility accessibility = Accessibility.NotApplicable, - DeclarationModifiers modifiers = default, - IEnumerable? members = null - ); - - [UnsafeAccessor(UnsafeAccessorKind.Method)] - private static extern SyntaxNode MethodDeclaration( - this SyntaxGenerator @this, - string name, - IEnumerable? parameters, - IEnumerable? typeParameters, - SyntaxNode? returnType, - Accessibility accessibility, - DeclarationModifiers modifiers, - IEnumerable? statements - ); - - [UnsafeAccessor(UnsafeAccessorKind.Method)] - private static extern SyntaxNode ParameterDeclaration( - this SyntaxGenerator @this, - string name, - SyntaxNode? type, - SyntaxNode? initializer, - RefKind refKind, - bool isExtension, - bool isParams, - bool isScoped - ); - - [UnsafeAccessor(UnsafeAccessorKind.Method)] - private static extern SyntaxNode OperatorDeclaration( - this SyntaxGenerator @this, - string operatorName, - bool isImplicitConversion, - IEnumerable? parameters = null, - SyntaxNode? returnType = null, - Accessibility accessibility = Accessibility.NotApplicable, - DeclarationModifiers modifiers = default, - IEnumerable? statements = null - ); - - [UnsafeAccessor(UnsafeAccessorKind.Method)] - private static extern SyntaxNode PropertyDeclaration( - this SyntaxGenerator @this, - string name, - SyntaxNode type, - SyntaxNode? getAccessor, - SyntaxNode? setAccessor, - Accessibility accessibility, - DeclarationModifiers modifiers - ); - - [UnsafeAccessor(UnsafeAccessorKind.Method)] - private static extern SyntaxNode SetAccessorDeclaration( - this SyntaxGenerator @this, - Accessibility accessibility, - bool isInitOnly, - IEnumerable? statements - ); - - [UnsafeAccessor(UnsafeAccessorKind.Method)] - private static extern SyntaxNode WithTypeParameters( - this SyntaxGenerator @this, - SyntaxNode declaration, - IEnumerable typeParameters - ); - - [UnsafeAccessor(UnsafeAccessorKind.Method)] - private static extern SyntaxNode WithTypeConstraint( - this SyntaxGenerator @this, - SyntaxNode declaration, - string typeParameterName, - SpecialTypeConstraintKind kinds, - bool isUnamangedType, - IEnumerable? types - ); -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.cs b/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.cs deleted file mode 100644 index f2fa19a..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/SyntaxGeneratorExtensions.cs +++ /dev/null @@ -1,44 +0,0 @@ -using AsmResolver.DotNet; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Editing; - -namespace CompatUnbreaker.Tool.SkeletonGeneration.CodeGeneration; - -internal static partial class SyntaxGeneratorExtensions -{ - public static TypeSyntax TypeExpression(this SyntaxGenerator syntaxGenerator, ITypeDescriptor typeDescriptor, TypeContext context) - { - return AsmResolverTypeSyntaxGenerator.TypeExpression(typeDescriptor, context); - } - - private static TypeSyntax TypeExpression(this SyntaxGenerator syntaxGenerator, ITypeDescriptor typeSymbol, RefKind refKind, TypeContext context) - { - var type = syntaxGenerator.TypeExpression(typeSymbol, context); - if (type is RefTypeSyntax refType) - { - type = refType.Type; - } - - return refKind switch - { - RefKind.Ref => SyntaxFactory.RefType(type), - RefKind.RefReadOnly => SyntaxFactory.RefType(SyntaxFactory.Token(SyntaxKind.RefKeyword), SyntaxFactory.Token(SyntaxKind.ReadOnlyKeyword), type), - _ => type, - }; - } - - public static SyntaxNode Declaration(this SyntaxGenerator syntaxGenerator, IMemberDefinition member, Func? memberFilter = null) - { - return member switch - { - TypeDefinition type => syntaxGenerator.TypeDeclaration(type, memberFilter), - MethodDefinition method => syntaxGenerator.MethodDeclaration(method), - FieldDefinition field => syntaxGenerator.FieldDeclaration(field), - PropertyDefinition property => syntaxGenerator.PropertyDeclaration(property), - EventDefinition @event => syntaxGenerator.EventDeclaration(@event), - _ => throw new ArgumentOutOfRangeException(nameof(member)), - }; - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/TypeContext.cs b/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/TypeContext.cs deleted file mode 100644 index b15e3ee..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/CodeGeneration/TypeContext.cs +++ /dev/null @@ -1,150 +0,0 @@ -using AsmResolver; -using AsmResolver.DotNet; -using AsmResolver.DotNet.Signatures; - -namespace CompatUnbreaker.Tool.SkeletonGeneration.CodeGeneration; - -internal record TypeContext( - TypeContext.GenericContext Generic, - TypeContext.TransformContext Transform -) -{ - public static TypeContext Empty { get; } = new(GenericContext.Empty, TransformContext.Empty); - - public sealed record GenericContext(TypeDefinition? Type, MethodDefinition? Method) - { - public static GenericContext Empty { get; } = new(null, null); - - public GenericParameter? GetGenericParameter(GenericParameterSignature parameter) - { - var genericParameters = parameter.ParameterType switch - { - GenericParameterType.Type => Type?.GenericParameters, - GenericParameterType.Method => Method?.GenericParameters, - _ => throw new ArgumentOutOfRangeException(nameof(parameter)), - }; - - if (genericParameters is null) - return null; - - if (parameter.Index >= 0 && parameter.Index < genericParameters.Count) - return genericParameters[parameter.Index]; - - return null; - } - } - - public sealed record TransformContext( - NullableAnnotation? DefaultNullableTransform = null, - NullableAnnotation[]? NullableTransforms = null, - bool[]? DynamicTransforms = null, - string?[]? TupleElementNames = null - ) - { - public static TransformContext Empty { get; } = new(); - - private int _nullablePosition; - private int _dynamicPosition; - private int _namesPosition; - - public NullableAnnotation? TryConsumeNullableTransform() - { - return NullableTransforms?[_nullablePosition++] ?? DefaultNullableTransform; - } - - public bool? TryConsumeDynamicTransform() - { - return DynamicTransforms?[_dynamicPosition++]; - } - - public Span ConsumeTupleElementNames(int numberOfElements) - { - if (TupleElementNames == null) return default; - - var result = TupleElementNames.AsSpan(_namesPosition, numberOfElements); - _namesPosition += numberOfElements; - return result; - } - - public static TransformContext From(NullableAnnotation? defaultNullableTransform, IHasCustomAttribute attributeProvider) - { - var nullableAttribute = attributeProvider.FindCustomAttributes("System.Runtime.CompilerServices", "NullableAttribute").SingleOrDefault(); - var nullableTransforms = nullableAttribute?.Signature!.FixedArguments.Single().Elements.Cast().Cast().ToArray(); - - var dynamicTransformsAttribute = attributeProvider.FindCustomAttributes("System.Runtime.CompilerServices", "DynamicAttribute").SingleOrDefault(); - bool[]? dynamicTransforms; - if (dynamicTransformsAttribute != null) - { - var arguments = dynamicTransformsAttribute.Signature!.FixedArguments; - dynamicTransforms = arguments.Count == 0 ? [true] : arguments.Single().Elements.Cast().ToArray(); - } - else - { - dynamicTransforms = null; - } - - var tupleElementNamesAttribute = attributeProvider.FindCustomAttributes("System.Runtime.CompilerServices", "TupleElementNamesAttribute").SingleOrDefault(); - var tupleElementNames = tupleElementNamesAttribute?.Signature!.FixedArguments.Single().Elements.Cast().Select(s => s?.Value).ToArray(); - - return new TransformContext( - defaultNullableTransform, - nullableTransforms, - dynamicTransforms, - tupleElementNames - ); - } - } - - public TypeContext WithTransformsAttributeProvider(IHasCustomAttribute attributeProvider) - { - return this with { Transform = TransformContext.From(Transform.DefaultNullableTransform, attributeProvider) }; - } - - public static TypeContext From(TypeDefinition type, IHasCustomAttribute? transformsAttributeProvider) - { - return From(type, null, transformsAttributeProvider); - } - - public static TypeContext From(MethodDefinition method, IHasCustomAttribute? transformsAttributeProvider) - { - return From(method.DeclaringType, method, transformsAttributeProvider); - } - - public static TypeContext From(IMemberDefinition member, IHasCustomAttribute? transformsAttributeProvider) - { - if (member is MethodDefinition method) return From(method, transformsAttributeProvider); - return From(member.DeclaringType, null, transformsAttributeProvider); - } - - private static TypeContext From(TypeDefinition? type, MethodDefinition? method, IHasCustomAttribute? transformsAttributeProvider) - { - var defaultNullableTransform = GetDefaultNullableTransform((IMemberDefinition?) method ?? type); - - return new TypeContext( - new GenericContext(type, method), - transformsAttributeProvider != null - ? TransformContext.From(defaultNullableTransform, transformsAttributeProvider) - : new TransformContext(defaultNullableTransform) - ); - } - - private static NullableAnnotation? GetDefaultNullableTransform(IMemberDefinition? member) - { - if (member == null) return null; - - var nullableContextAttribute = ((IHasCustomAttribute) member).FindCustomAttributes("System.Runtime.CompilerServices", "NullableContextAttribute").SingleOrDefault(); - - if (nullableContextAttribute != null) - { - ArgumentNullException.ThrowIfNull(nullableContextAttribute.Signature); - return (NullableAnnotation) (byte) nullableContextAttribute.Signature.FixedArguments.Single().Element!; - } - - if (member.DeclaringType != null) - { - return GetDefaultNullableTransform(member.DeclaringType); - } - - return null; - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/MethodBodyRewriter.cs b/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/MethodBodyRewriter.cs deleted file mode 100644 index a560634..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/MethodBodyRewriter.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace CompatUnbreaker.Tool.SkeletonGeneration; - -internal sealed class MethodBodyRewriter : CSharpSyntaxRewriter -{ - public static MethodBodyRewriter Instance { get; } = new(); - - public static BlockSyntax Body { get; } = SyntaxFactory.Block(SyntaxFactory.ParseStatement("throw new NotImplementedException();")); - public static ArrowExpressionClauseSyntax ExpressionBody { get; } = SyntaxFactory.ArrowExpressionClause(SyntaxFactory.ParseExpression("throw new NotImplementedException()")); - - [return: NotNullIfNotNull(nameof(node))] - public override SyntaxNode? Visit(SyntaxNode? node) - { - if (node is BaseMethodDeclarationSyntax methodNode) - { - node = VisitBaseMethodDeclarationSyntax(methodNode); - } - - return base.Visit(node); - } - - private SyntaxNode VisitBaseMethodDeclarationSyntax(BaseMethodDeclarationSyntax node) - { - if (node.Body != null) - { - return node.WithBody(Body); - } - - return node; - } - - public override SyntaxNode VisitAccessorDeclaration(AccessorDeclarationSyntax node) - { - // if (node.Parent?.Parent is BasePropertyDeclarationSyntax property - // && (property.Modifiers.Any(SyntaxKind.AbstractKeyword) || property.Modifiers.Any(SyntaxKind.ExternKeyword))) - // { - // return node; - // } - if (node.Body == null && node.ExpressionBody == null) - { - return node; - } - - return node.WithBody(null).WithExpressionBody(ExpressionBody).WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)); - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/SkeletonGenerator.cs b/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/SkeletonGenerator.cs deleted file mode 100644 index b12a79f..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/SkeletonGeneration/SkeletonGenerator.cs +++ /dev/null @@ -1,632 +0,0 @@ -using System.Diagnostics; -using System.Reflection; -using AsmResolver; -using AsmResolver.DotNet; -using AsmResolver.DotNet.Signatures; -using CompatUnbreaker.Tool.ApiCompatibility.AssemblyMapping; -using CompatUnbreaker.Tool.ApiCompatibility.Comparing; -using CompatUnbreaker.Tool.ApiCompatibility.Comparing.Rules; -using CompatUnbreaker.Tool.SkeletonGeneration.CodeGeneration; -using CompatUnbreaker.Tool.Utilities; -using CompatUnbreaker.Tool.Utilities.AsmResolver; -using CompatUnbreaker.Tool.Utilities.Roslyn; -using CompatUnbreaker.Utilities.AsmResolver; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Editing; -using Microsoft.CodeAnalysis.Formatting; -using Microsoft.CodeAnalysis.MSBuild; -using Microsoft.CodeAnalysis.Simplification; -using MonoMod.RuntimeDetour; -using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; - -namespace CompatUnbreaker.Tool.SkeletonGeneration; - -internal static class SkeletonGenerator -{ - public static async Task<(MSBuildWorkspace Workspace, Project Project)> GenerateAsync(AssemblyMapper assemblyMapper, string projectPath) - { - var workspace = MSBuildWorkspace.Create(); - var project = await workspace.OpenProjectAsync(projectPath); - - return (workspace, await GenerateAsync(assemblyMapper, workspace, project)); - } - - public static async Task GenerateAsync(AssemblyMapper assemblyMapper, Workspace workspace, Project project) - { - using var hook = new Hook( - Type.GetType("Microsoft.CodeAnalysis.CSharp.Extensions.SemanticModelExtensions, Microsoft.CodeAnalysis.CSharp.Workspaces")! - .GetMethod("UnifiesNativeIntegers", BindingFlags.Public | BindingFlags.Static)!, - bool (SemanticModel _) => false - ); - - var compilation = await project.GetCompilationAsync() ?? throw new InvalidOperationException("Failed to get compilation"); - var shimTypes = compilation.Assembly.GlobalNamespace.GetAllTypes().ToArray(); - var shimExtensionTypes = shimTypes.Where(t => - t.ContainingType == null && - t.GetAttributes().Any(a => a.AttributeClass?.GetFullMetadataName() == "CompatUnbreaker.Attributes.UnbreakerExtensionsAttribute") - ).SelectMany(t => t.GetTypeMembers()).ToArray(); - - var topLevelTypeDifferences = new Dictionary(); - var memberDifferences = new Dictionary>(); - - void AddMemberDifference(TypeMapper typeMapper, CompatDifference difference) - { - if (!memberDifferences.TryGetValue(typeMapper, out var list)) - { - memberDifferences.Add(typeMapper, list = []); - } - - list.Add(difference); - } - - var apiComparer = new ApiComparer(); - apiComparer.Compare(assemblyMapper); - - foreach (var difference in apiComparer.CompatDifferences) - { - Console.WriteLine(difference); - - switch (difference) - { - case TypeMustExistDifference typeDifference: - { - if (typeDifference.Mapper.DeclaringType is { } declaringType) - { - AddMemberDifference(declaringType, typeDifference); - break; - } - - topLevelTypeDifferences.Add(typeDifference.Mapper, typeDifference); - break; - } - - case MemberMustExistDifference memberDifference: - { - var typeMapper = memberDifference.Mapper.DeclaringType; - AddMemberDifference(typeMapper, memberDifference); - break; - } - - default: - break; - } - } - - Document AddDocument(TypeDefinition type, SyntaxNode syntaxRoot) - { - var name = type.Name + ".cs"; - - var folders = type.Namespace is null - ? [] - : type.Namespace.Value.TrimPrefix(project.DefaultNamespace).Split('.', StringSplitOptions.RemoveEmptyEntries); - - if (project.Documents.Any(d => d.Folders.SequenceEqual(folders) && d.Name == name)) - { - throw new InvalidOperationException($"Duplicate document '{(folders.Length == 0 ? string.Empty : string.Join('/', folders))}{name}'"); - } - - return project.AddDocument(name, syntaxRoot, folders); - } - - var modifiedDocuments = new HashSet(); - - foreach (var (mapper, difference) in topLevelTypeDifferences) - { - if (difference is not TypeMustExistDifference) continue; - - var (leftType, rightType) = mapper; - if (leftType == null || rightType != null) continue; - - var syntaxGenerator = SyntaxGenerator.GetGenerator(project); - - var typeDeclaration = CreateDeclaration(syntaxGenerator, leftType); - - var syntaxRoot = CreateCompilationUnit(syntaxGenerator, leftType.Namespace, typeDeclaration); - - syntaxRoot = (CompilationUnitSyntax) MethodBodyRewriter.Instance.Visit(syntaxRoot); - - var document = AddDocument(leftType, syntaxRoot); - modifiedDocuments.Add(document.Id); - project = document.Project; - } - - foreach (var (typeMapper, differences) in memberDifferences) - { - var (leftType, rightType) = typeMapper; - if (leftType == null) continue; - - var missingMembers = new List(); - - foreach (var difference in differences) - { - IMemberDefinition? left; - IMemberDefinition? right; - - switch (difference) - { - case TypeMustExistDifference typeMustExistDifference: - (left, right) = typeMustExistDifference.Mapper; - break; - - case MemberMustExistDifference memberMustExistDifference: - (left, right) = memberMustExistDifference.Mapper; - break; - - default: - throw new UnreachableException(); - } - - if (left == null || right != null) continue; - - missingMembers.Add(left); - } - - var syntaxGenerator = SyntaxGenerator.GetGenerator(project); - - ITypeSymbol? existingShimType = compilation.Assembly.GetTypeByMetadataName(leftType.FullName); - existingShimType ??= shimTypes.SingleOrDefault(t => - t.GetAttributes().Any(a => - a.AttributeClass?.GetFullMetadataName() == "CompatUnbreaker.Attributes.UnbreakerReplaceAttribute" && - ((ITypeSymbol) a.ConstructorArguments.Single().Value!).GetFullMetadataName() == leftType.FullName - ) - ); - - if (existingShimType != null) - { - var syntaxReference = existingShimType.DeclaringSyntaxReferences.First(); - var document = project.GetDocument(syntaxReference.SyntaxTree); - var syntaxNode = await syntaxReference.GetSyntaxAsync(); - - var editor = await DocumentEditor.CreateAsync(document); - - // TODO insert members preserving member kind order - foreach (var member in missingMembers) - { - if (!syntaxGenerator.CanBeDeclared(member)) continue; - editor.AddMember(syntaxNode, CreateDeclaration(syntaxGenerator, member)); - } - - document = editor.GetChangedDocument(); - modifiedDocuments.Add(document.Id); - project = document.Project; - } - else - { - var existingShimNativeExtensionType = shimExtensionTypes.FirstOrDefault(t => - t.ExtensionParameter != null && - t.ExtensionParameter.Type.GetFullMetadataName() == leftType.FullName - )?.DeclaringSyntaxReferences.First(); - - var existingShimUnbreakerExtensionType = shimExtensionTypes.FirstOrDefault(t => - t.GetAttributes().Any(a => - a.AttributeClass?.GetFullMetadataName() == "CompatUnbreaker.Attributes.UnbreakerExtensionAttribute" && - ((ITypeSymbol) a.ConstructorArguments.Single().Value!).GetFullMetadataName() == leftType.FullName - ) - )?.DeclaringSyntaxReferences.First(); - - Document? document = null; - SyntaxNode? extensionsType = null; - - if (existingShimNativeExtensionType != null || existingShimUnbreakerExtensionType != null) - { - document = project.GetDocument((existingShimNativeExtensionType ?? existingShimUnbreakerExtensionType)!.SyntaxTree); - extensionsType = (await (existingShimNativeExtensionType ?? existingShimUnbreakerExtensionType)!.GetSyntaxAsync()).Parent; - } - - var (nativeMembers, unbreakerMembers) = CreateExtensionMembers(syntaxGenerator, missingMembers); - - if (nativeMembers.Count > 0) - { - await EditOrAddDocumentAsync(nativeMembers, existingShimNativeExtensionType, members => - { - return ExtensionDeclaration() - .AddParameterListParameters( - Parameter(leftType.IsStatic() ? default : "this".ToIdentifierToken()) - .WithType(syntaxGenerator.TypeExpression(leftType, TypeContext.Empty)) - ) - .WithOpenBraceToken(Token(SyntaxKind.OpenBraceToken)) - .WithCloseBraceToken(Token(SyntaxKind.CloseBraceToken)) - .WithMembers( - List(members) - ); - }); - } - - if (unbreakerMembers.Count > 0) - { - await EditOrAddDocumentAsync(unbreakerMembers, existingShimUnbreakerExtensionType, members => - { - MemberDeclarationSyntax node = ClassDeclaration($"{leftType.GetUnmangledName()}Extension") - .AddAttributeLists( - (AttributeListSyntax) syntaxGenerator.Attribute( - "UnbreakerExtension", - TypeOfExpression(syntaxGenerator.TypeExpression(leftType, TypeContext.Empty)) - ) - ) - .WithModifiers( - TokenList(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.StaticKeyword)) - ) - .WithMembers( - List(members) - ); - - node = (MemberDeclarationSyntax) syntaxGenerator.WithTypeParametersAndConstraints(node, leftType.GenericParameters, TypeContext.From(leftType, leftType)); - - return node; - }); - } - - async Task EditOrAddDocumentAsync(List members, SyntaxReference? extensionType, Func, MemberDeclarationSyntax> factory) - { - if (extensionType != null) - { - var syntaxNode = await extensionType.GetSyntaxAsync(); - - var editor = await DocumentEditor.CreateAsync(document); - - // TODO insert members preserving member kind order - foreach (var member in members) - { - editor.AddMember(syntaxNode, member.WithSimplifyAnnotations()); - } - - document = editor.GetChangedDocument(); - } - else if (document != null) - { - Debug.Assert(extensionsType != null); - - var editor = await DocumentEditor.CreateAsync(document); - - editor.AddMember(extensionsType, factory(members)); - - document = editor.GetChangedDocument(); - } - else - { - if (typeMapper.DeclaringType != null) - { - throw new NotImplementedException(); - } - - MemberDeclarationSyntax node = ClassDeclaration($"{leftType.GetUnmangledName()}Extensions") - .AddAttributeLists( - (AttributeListSyntax) syntaxGenerator.Attribute("UnbreakerExtensions") - ) - .WithModifiers( - TokenList(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.StaticKeyword)) - ) - .WithMembers( - List([factory(members)]) - ); - - var syntaxRoot = CreateCompilationUnit(syntaxGenerator, leftType.Namespace, node); - - document = AddDocument(leftType, syntaxRoot); - extensionsType = (await document.GetSyntaxRootAsync())!.DescendantNodes().OfType().First(); - } - - modifiedDocuments.Add(document.Id); - project = document.Project; - } - } - } - - foreach (var documentId in modifiedDocuments) - { - var document = project.GetDocument(documentId)!; - - document = await SimplifyAsync(document); - - Console.WriteLine((await document.GetTextAsync()).ToString()); - project = document.Project; - } - - Console.Write("Enter `y` to save: "); - if (Console.ReadKey().KeyChar == 'y') - { - // TODO stupid - var projectText = await File.ReadAllTextAsync(project.FilePath); - - if (!workspace.TryApplyChanges(project.Solution)) - { - Console.WriteLine("Failed to apply changes"); - } - - await File.WriteAllTextAsync(project.FilePath, projectText); - } - - return project; - } - - private static async Task SimplifyAsync(Document document) - { - document = await ImportAdder.AddImportsAsync(document, Simplifier.AddImportsAnnotation); - - // ImportAdder adds leading trivia before the first member, but it's not cleaned up when Simplifier removes all usings, so just let Formatter handle it - { - var syntaxRoot = (CompilationUnitSyntax) (await document.GetSyntaxRootAsync())!; - var firstNode = syntaxRoot.Members.First(); - document = document.WithSyntaxRoot(syntaxRoot.ReplaceNode(firstNode, firstNode.WithoutLeadingTrivia())); - } - - document = await Simplifier.ReduceAsync(document, Simplifier.Annotation); - document = await Formatter.FormatAsync(document, Formatter.Annotation); - document = await Formatter.FormatAsync(document, SyntaxAnnotation.ElasticAnnotation); - - return document; - } - - private static MemberDeclarationSyntax CreateDeclaration(SyntaxGenerator syntaxGenerator, IMemberDefinition member) - { - var declaration = syntaxGenerator.Declaration(member, m => m.IsVisibleOutsideOfAssembly()).WithSimplifyAnnotations(); - declaration = MethodBodyRewriter.Instance.Visit(declaration); - return (MemberDeclarationSyntax) declaration; - } - - private static CompilationUnitSyntax CreateCompilationUnit(SyntaxGenerator syntaxGenerator, Utf8String? @namespace, MemberDeclarationSyntax node) - { - if (@namespace is not null) - { - var namespaceSyntax = (NameSyntax) syntaxGenerator.DottedName(@namespace); - node = FileScopedNamespaceDeclaration(namespaceSyntax) - .AddMembers(node); - } - - return CompilationUnit() - .AddMembers(node) - .WithTrailingTrivia(LineFeed) - .WithSimplifyAnnotations(); - } - - private static TNode WithSimplifyAnnotations(this TNode node) where TNode : SyntaxNode - { - return node.WithAdditionalAnnotations(Simplifier.Annotation, Simplifier.AddImportsAnnotation, Formatter.Annotation); - } - - private static (List NativeMembers, List UnbreakerMembers) CreateExtensionMembers(SyntaxGenerator syntaxGenerator, List members) - { - var nativeMembers = new List(); - var unbreakerMembers = new List(); - - foreach (var member in members) - { - if (member is TypeDefinition type) - { - unbreakerMembers.Add(CreateDeclaration(syntaxGenerator, type)); - } - else if (member is FieldDefinition field) - { - var fieldType = syntaxGenerator.TypeExpression(field.Signature!.FieldType, TypeContext.From(field, field)).ToString(); - var fieldName = field.Name!.Value.EscapeIdentifier(); - - var body = field.IsInitOnly - ? " => throw new NotImplementedException();" - : """ - - { - get => throw new NotImplementedException(); - set => throw new NotImplementedException(); - } - """; - - nativeMembers.Add(ParseMemberDeclaration( - $""" - [UnbreakerField] - public {(field.IsStatic ? "static " : string.Empty)}{fieldType} {fieldName}{body} - """ - )!.WithLeadingTrivia(LineFeed).WithTrailingTrivia(LineFeed).WithSimplifyAnnotations()); - } - else if (member is MethodDefinition method) - { - if (method.IsPropertyAccessor()) - { - continue; - } - - if (method.IsEventAccessor() && member.IsStatic()) - { - continue; - } - - if (method.IsConstructor) - { - method.Name = "Create"; - method.IsStatic = true; - method.Signature = MethodSignature.CreateStatic(method.DeclaringType.ToTypeSignature(), method.Signature.ParameterTypes); - - var declaration = (BaseMethodDeclarationSyntax) CreateDeclaration(syntaxGenerator, method); - - declaration = declaration.WithAttributeLists(declaration.AttributeLists.Insert(0, AttributeList([Attribute(IdentifierName("UnbreakerConstructor"))]))); - - unbreakerMembers.Add(declaration); - } - else - { - var declaration = (BaseMethodDeclarationSyntax) CreateDeclaration(syntaxGenerator, method); - - if (method.IsExtension()) - { - var firstParameter = declaration.ParameterList.Parameters[0]; - declaration = declaration.WithParameterList(declaration.ParameterList.WithParameters( - declaration.ParameterList.Parameters.Replace( - firstParameter, - firstParameter - .WithModifiers(firstParameter.Modifiers.Remove(firstParameter.Modifiers.Single(m => m.IsKind(SyntaxKind.ThisKeyword)))) - .WithAttributeLists(firstParameter.AttributeLists.Insert(0, AttributeList([Attribute(IdentifierName("UnbreakerThis"))]))) - ) - )); - } - - nativeMembers.Add(declaration); - } - } - else if (member is PropertyDefinition property) - { - nativeMembers.Add(CreateDeclaration(syntaxGenerator, property)); - } - else if (member is EventDefinition @event) - { - if (!@event.IsStatic()) - { - continue; // TODO - throw new NotImplementedException(); - } - - unbreakerMembers.Add(CreateDeclaration(syntaxGenerator, @event)); - } - else - { - throw new UnreachableException(); - } - } - - return (nativeMembers, unbreakerMembers); - } - - // private static MemberDeclarationSyntax CreateMemberFromClassDeclaration( - // SyntaxGenerator syntaxGenerator, - // T member - // ) where T : IMemberDefinition, IHasCustomAttribute - // { - // if (member.IsStatic()) - // { - // throw new NotImplementedException(); - // } - // - // var targetType = syntaxGenerator.TypeExpression(GetParameterType(member.DeclaringType!), TypeContext.Empty); - // var propertyType = syntaxGenerator.TypeExpression( - // member switch - // { - // PropertyDefinition property => property.Signature!.ReturnType, - // FieldDefinition field => field.Signature!.FieldType, - // _ => throw new ArgumentOutOfRangeException(nameof(member), member, null), - // }, - // TypeContext.From(member, member) - // ); - // - // var hasGetMethod = member switch - // { - // PropertyDefinition property => property.GetMethod != null, - // FieldDefinition => true, - // _ => throw new ArgumentOutOfRangeException(nameof(member), member, null), - // }; - // - // var hasSetMethod = member switch - // { - // PropertyDefinition property => property.SetMethod != null, - // FieldDefinition field => !field.IsInitOnly, - // _ => throw new ArgumentOutOfRangeException(nameof(member), member, null), - // }; - // - // var accessors = new List(); - // - // var thisParameter = syntaxGenerator.ParameterDeclaration("@this", targetType); - // - // if (hasGetMethod) - // { - // accessors.Add(syntaxGenerator.MethodDeclaration( - // accessibility: Accessibility.Public, - // modifiers: DeclarationModifiers.Static, - // returnType: propertyType, - // name: "Get", - // parameters: [thisParameter] - // )); - // } - // - // if (hasSetMethod) - // { - // accessors.Add(syntaxGenerator.MethodDeclaration( - // accessibility: Accessibility.Public, - // modifiers: DeclarationModifiers.Static, - // name: "Set", - // parameters: - // [ - // thisParameter, - // syntaxGenerator.ParameterDeclaration("value", propertyType), - // ] - // )); - // } - // - // var declaration = syntaxGenerator.ClassDeclaration( - // member.Name!.EscapeIdentifier(), - // accessibility: Accessibility.Public, - // modifiers: DeclarationModifiers.Static, - // members: accessors - // ); - // - // declaration = declaration.ReplaceNodes( - // declaration.DescendantNodes().OfType(), - // (node, _) => node - // .WithBody(null) - // .WithExpressionBody(MethodBodyRewriter.ExpressionBody) - // .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) - // ); - // - // return (MemberDeclarationSyntax) syntaxGenerator.AddAttributes( - // declaration, - // syntaxGenerator.Attribute(member switch - // { - // PropertyDefinition => "CompatUnbreaker.Attributes.UnbreakerPropertyAttribute", - // FieldDefinition => "CompatUnbreaker.Attributes.UnbreakerFieldAttribute", - // EventDefinition => "CompatUnbreaker.Attributes.UnbreakerEventAttribute", - // _ => throw new ArgumentOutOfRangeException(nameof(member), member, null), - // }) - // ); - // } - - // private static void ConvertInstanceMethodToExtension(MethodDefinition method) - // { - // if (method.IsStatic) return; - // - // ArgumentNullException.ThrowIfNull(method.DeclaringType); - // ArgumentNullException.ThrowIfNull(method.Signature); - // - // var module = method.Module; - // ArgumentNullException.ThrowIfNull(module); - // - // method.CustomAttributes.Add(new CustomAttribute( - // new MemberReference( - // new TypeReference(module, module.CorLibTypeFactory.CorLibScope, "System.Runtime.CompilerServices"u8, "ExtensionAttribute"u8), - // ".ctor"u8, - // MethodSignature.CreateInstance(module.CorLibTypeFactory.Void) - // ) - // )); - // - // method.IsStatic = true; - // - // method.Signature.HasThis = false; - // method.Signature.ParameterTypes.Insert(0, GetParameterType(method.DeclaringType)); - // - // foreach (var definition in method.ParameterDefinitions) - // { - // definition.Sequence++; - // } - // - // method.Parameters.PullUpdatesFromMethodSignature(); - // method.Parameters[0].GetOrCreateDefinition().Name = "this"; - // } - - // private static TypeSignature GetParameterType(TypeDefinition type) - // { - // TypeSignature result; - // if (type.GenericParameters.Count > 0) - // { - // var genArgs = new TypeSignature[type.GenericParameters.Count]; - // for (var i = 0; i < genArgs.Length; i++) - // genArgs[i] = new GenericParameterSignature(type.Module, GenericParameterType.Type, i); - // result = type.MakeGenericInstanceType(genArgs); - // } - // else - // { - // result = type.ToTypeSignature(); - // } - // - // if (type.IsValueType) - // result = result.MakeByReferenceType(); - // - // return result; - // } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/Utilities/Roslyn/TypeSymbolExtensions.cs b/src/build/CompatUnbreaker/CompatUnbreaker.Tool/Utilities/Roslyn/TypeSymbolExtensions.cs deleted file mode 100644 index ac0569a..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/Utilities/Roslyn/TypeSymbolExtensions.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.Text; -using Microsoft.CodeAnalysis; - -namespace CompatUnbreaker.Tool.Utilities.Roslyn; - -internal static class TypeSymbolExtensions -{ - public static IEnumerable GetAllTypes(this INamespaceOrTypeSymbol symbol) - { - var queue = new Queue(); - queue.Enqueue(symbol); - - while (queue.Count > 0) - { - var member = queue.Dequeue(); - - if (member is INamespaceSymbol namespaceSymbol) - { - foreach (var namespaceMember in namespaceSymbol.GetMembers()) - { - queue.Enqueue(namespaceMember); - } - } - else if (member is ITypeSymbol namedTypeSymbol) - { - yield return namedTypeSymbol; - - foreach (var typeMember in namedTypeSymbol.GetTypeMembers()) - { - queue.Enqueue(typeMember); - } - } - else - { - throw new InvalidOperationException(); - } - } - } - - public static string GetFullMetadataName(this ITypeSymbol s) - { - var stringBuilder = new StringBuilder(s.MetadataName); - var current = s.ContainingSymbol; - - while (current is not INamespaceSymbol { IsGlobalNamespace: true }) - { - if (current is ITypeSymbol) - { - stringBuilder.Insert(0, "+"); - } - else if (current is INamespaceSymbol) - { - stringBuilder.Insert(0, "."); - } - - stringBuilder.Insert(0, current.MetadataName); - current = current.ContainingSymbol; - } - - return stringBuilder.ToString(); - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/packages.lock.json b/src/build/CompatUnbreaker/CompatUnbreaker.Tool/packages.lock.json deleted file mode 100644 index 675eedd..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker.Tool/packages.lock.json +++ /dev/null @@ -1,506 +0,0 @@ -{ - "version": 1, - "dependencies": { - "net9.0": { - "Basic.Reference.Assemblies.Net90": { - "type": "Direct", - "requested": "[1.8.3, )", - "resolved": "1.8.3", - "contentHash": "sZRH0NW/jdLTNq8X2/IVGIUVVE0JLKxwSp9GOwKzoHzt7BK5U7iRqG25ddSsggdABpvyz3ooQ4GkwQQ/Kn+z+g==", - "dependencies": { - "Microsoft.CodeAnalysis.Common": "4.11.0" - } - }, - "ConsoleAppFramework": { - "type": "Direct", - "requested": "[5.6.2, )", - "resolved": "5.6.2", - "contentHash": "IAc33TLfXAM6bVQFIsvr8I7+bC3TGU9xiyuG8aRtCzIPFctvB28bchZP8BhI/7cuSNlhQDDRZb0YsmrhL4Lfcg==" - }, - "Meziantou.Analyzer": { - "type": "Direct", - "requested": "[2.0.220, )", - "resolved": "2.0.220", - "contentHash": "LknH5tduuxY6jzPGUIBHrszk1fepyEWAD9Ht7Hq1rrGZ02BKx54ADXzrlXTkLNpnkVzRBHU4bO6HH7GwIeDR+w==" - }, - "Microsoft.Build": { - "type": "Direct", - "requested": "[17.14.28, )", - "resolved": "17.14.28", - "contentHash": "MmGLEsROW1C9dH/d4sOqUX0sVNs2uwTCFXRQb89+pYNWDNJE+7bTJG9kOCbHeCH252XLnP55KIaOgwSpf6J4Kw==", - "dependencies": { - "Microsoft.Build.Framework": "17.14.28", - "Microsoft.NET.StringTools": "17.14.28", - "System.Configuration.ConfigurationManager": "9.0.0", - "System.Diagnostics.EventLog": "9.0.0", - "System.Reflection.MetadataLoadContext": "9.0.0", - "System.Security.Cryptography.ProtectedData": "9.0.0" - } - }, - "Microsoft.Build.Framework": { - "type": "Direct", - "requested": "[17.14.28, )", - "resolved": "17.14.28", - "contentHash": "wRcyTzGV0LRAtFdrddtioh59Ky4/zbvyraP0cQkDzRSRkhgAQb0K88D/JNC6VHLIXanRi3mtV1jU0uQkBwmiVg==" - }, - "Microsoft.Build.Locator": { - "type": "Direct", - "requested": "[1.10.2, )", - "resolved": "1.10.2", - "contentHash": "F+nLS7IpgtslyxNvtD6Jalnf5WU08lu8yfJBNQl3cbEF3AMUphs4t7nPuRYaaU8QZyGrqtVi7i73LhAe/yHx7A==" - }, - "Microsoft.Build.Tasks.Core": { - "type": "Direct", - "requested": "[17.14.28, )", - "resolved": "17.14.28", - "contentHash": "jk3O0tXp9QWPXhLJ7Pl8wm/eGtGgA1++vwHGWEmnwMU6eP//ghtcCUpQh9CQMwEKGDnH0aJf285V1s8yiSlKfQ==", - "dependencies": { - "Microsoft.Build.Framework": "17.14.28", - "Microsoft.Build.Utilities.Core": "17.14.28", - "Microsoft.NET.StringTools": "17.14.28", - "System.CodeDom": "9.0.0", - "System.Collections.Immutable": "9.0.0", - "System.Configuration.ConfigurationManager": "9.0.0", - "System.Diagnostics.EventLog": "9.0.0", - "System.Formats.Nrbf": "9.0.0", - "System.Resources.Extensions": "9.0.0", - "System.Security.Cryptography.Pkcs": "9.0.0", - "System.Security.Cryptography.ProtectedData": "9.0.0", - "System.Security.Cryptography.Xml": "9.0.0" - } - }, - "Microsoft.Build.Utilities.Core": { - "type": "Direct", - "requested": "[17.14.28, )", - "resolved": "17.14.28", - "contentHash": "rhSdPo8QfLXXWM+rY0x0z1G4KK4ZhMoIbHROyDj8MUBFab9nvHR0NaMnjzOgXldhmD2zi2ir8d6xCatNzlhF5g==", - "dependencies": { - "Microsoft.Build.Framework": "17.14.28", - "Microsoft.NET.StringTools": "17.14.28", - "System.Collections.Immutable": "9.0.0", - "System.Configuration.ConfigurationManager": "9.0.0", - "System.Diagnostics.EventLog": "9.0.0", - "System.Security.Cryptography.ProtectedData": "9.0.0" - } - }, - "Microsoft.CodeAnalysis.CSharp": { - "type": "Direct", - "requested": "[4.14.0, )", - "resolved": "4.14.0", - "contentHash": "568a6wcTivauIhbeWcCwfWwIn7UV7MeHEBvFB2uzGIpM2OhJ4eM/FZ8KS0yhPoNxnSpjGzz7x7CIjTxhslojQA==", - "dependencies": { - "Microsoft.CodeAnalysis.Analyzers": "3.11.0", - "Microsoft.CodeAnalysis.Common": "[4.14.0]", - "System.Collections.Immutable": "9.0.0", - "System.Reflection.Metadata": "9.0.0" - } - }, - "Microsoft.CodeAnalysis.CSharp.Workspaces": { - "type": "Direct", - "requested": "[4.14.0, )", - "resolved": "4.14.0", - "contentHash": "QkgCEM4qJo6gdtblXtNgHqtykS61fxW+820hx5JN6n9DD4mQtqNB+6fPeJ3GQWg6jkkGz6oG9yZq7H3Gf0zwYw==", - "dependencies": { - "Humanizer.Core": "2.14.1", - "Microsoft.CodeAnalysis.Analyzers": "3.11.0", - "Microsoft.CodeAnalysis.CSharp": "[4.14.0]", - "Microsoft.CodeAnalysis.Common": "[4.14.0]", - "Microsoft.CodeAnalysis.Workspaces.Common": "[4.14.0]", - "System.Collections.Immutable": "9.0.0", - "System.Composition": "9.0.0", - "System.IO.Pipelines": "9.0.0", - "System.Reflection.Metadata": "9.0.0", - "System.Threading.Channels": "7.0.0" - } - }, - "Microsoft.CodeAnalysis.Workspaces.MSBuild": { - "type": "Direct", - "requested": "[4.14.0, )", - "resolved": "4.14.0", - "contentHash": "YU7Sguzm1Cuhi2U6S0DRKcVpqAdBd2QmatpyE0KqYMJogJ9E27KHOWGUzAOjsyjAM7sNaUk+a8VPz24knDseFw==", - "dependencies": { - "Humanizer.Core": "2.14.1", - "Microsoft.Build": "17.7.2", - "Microsoft.Build.Framework": "17.7.2", - "Microsoft.Build.Tasks.Core": "17.7.2", - "Microsoft.Build.Utilities.Core": "17.7.2", - "Microsoft.CodeAnalysis.Analyzers": "3.11.0", - "Microsoft.CodeAnalysis.Workspaces.Common": "[4.14.0]", - "Microsoft.Extensions.DependencyInjection": "9.0.0", - "Microsoft.Extensions.Logging": "9.0.0", - "Microsoft.Extensions.Logging.Abstractions": "9.0.0", - "Microsoft.Extensions.Options": "9.0.0", - "Microsoft.Extensions.Primitives": "9.0.0", - "Newtonsoft.Json": "13.0.3", - "System.CodeDom": "7.0.0", - "System.Collections.Immutable": "9.0.0", - "System.Composition": "9.0.0", - "System.Configuration.ConfigurationManager": "9.0.0", - "System.Diagnostics.EventLog": "9.0.0", - "System.IO.Pipelines": "9.0.0", - "System.Reflection.Metadata": "9.0.0", - "System.Resources.Extensions": "9.0.0", - "System.Security.Cryptography.Pkcs": "7.0.2", - "System.Security.Cryptography.ProtectedData": "9.0.0", - "System.Security.Cryptography.Xml": "7.0.1", - "System.Security.Permissions": "9.0.0", - "System.Text.Json": "9.0.0", - "System.Threading.Channels": "7.0.0", - "System.Threading.Tasks.Dataflow": "9.0.0", - "System.Windows.Extensions": "9.0.0" - } - }, - "MonoMod.RuntimeDetour": { - "type": "Direct", - "requested": "[25.3.1, )", - "resolved": "25.3.1", - "contentHash": "QQ9Ng3E6enCMbcFNDSUqWBD4+KdnjfxConI8QzMAH9p7i2vvzehqYT8K4Mghq2YeTIVN6Aih8PlqcQxQk2uypQ==", - "dependencies": { - "Mono.Cecil": "0.11.6", - "MonoMod.Backports": "1.1.2", - "MonoMod.Core": "1.3.1", - "MonoMod.ILHelpers": "1.1.0", - "MonoMod.Utils": "25.0.9" - } - }, - "PolySharp": { - "type": "Direct", - "requested": "[1.15.0, )", - "resolved": "1.15.0", - "contentHash": "FbU0El+EEjdpuIX4iDbeS7ki1uzpJPx8vbqOzEtqnl1GZeAGJfq+jCbxeJL2y0EPnUNk8dRnnqR2xnYXg9Tf+g==" - }, - "StyleCop.Analyzers": { - "type": "Direct", - "requested": "[1.2.0-beta.556, )", - "resolved": "1.2.0-beta.556", - "contentHash": "llRPgmA1fhC0I0QyFLEcjvtM2239QzKr/tcnbsjArLMJxJlu0AA5G7Fft0OI30pHF3MW63Gf4aSSsjc5m82J1Q==", - "dependencies": { - "StyleCop.Analyzers.Unstable": "1.2.0.556" - } - }, - "AsmResolver": { - "type": "Transitive", - "resolved": "6.0.0-dev" - }, - "AsmResolver.DotNet": { - "type": "Transitive", - "resolved": "6.0.0-dev", - "dependencies": { - "AsmResolver.PE": "6.0.0-dev" - } - }, - "AsmResolver.PE": { - "type": "Transitive", - "resolved": "6.0.0-dev", - "dependencies": { - "AsmResolver.PE.File": "6.0.0-dev" - } - }, - "AsmResolver.PE.File": { - "type": "Transitive", - "resolved": "6.0.0-dev", - "dependencies": { - "AsmResolver": "6.0.0-dev" - } - }, - "Humanizer.Core": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==" - }, - "Microsoft.CodeAnalysis.Analyzers": { - "type": "Transitive", - "resolved": "3.11.0", - "contentHash": "v/EW3UE8/lbEYHoC2Qq7AR/DnmvpgdtAMndfQNmpuIMx/Mto8L5JnuCfdBYtgvalQOtfNCnxFejxuRrryvUTsg==" - }, - "Microsoft.CodeAnalysis.Common": { - "type": "Transitive", - "resolved": "4.14.0", - "contentHash": "PC3tuwZYnC+idaPuoC/AZpEdwrtX7qFpmnrfQkgobGIWiYmGi5MCRtl5mx6QrfMGQpK78X2lfIEoZDLg/qnuHg==", - "dependencies": { - "Microsoft.CodeAnalysis.Analyzers": "3.11.0", - "System.Collections.Immutable": "9.0.0", - "System.Reflection.Metadata": "9.0.0" - } - }, - "Microsoft.CodeAnalysis.Workspaces.Common": { - "type": "Transitive", - "resolved": "4.14.0", - "contentHash": "wNVK9JrqjqDC/WgBUFV6henDfrW87NPfo98nzah/+M/G1D6sBOPtXwqce3UQNn+6AjTnmkHYN1WV9XmTlPemTw==", - "dependencies": { - "Humanizer.Core": "2.14.1", - "Microsoft.CodeAnalysis.Analyzers": "3.11.0", - "Microsoft.CodeAnalysis.Common": "[4.14.0]", - "System.Collections.Immutable": "9.0.0", - "System.Composition": "9.0.0", - "System.IO.Pipelines": "9.0.0", - "System.Reflection.Metadata": "9.0.0", - "System.Threading.Channels": "7.0.0" - } - }, - "Microsoft.Extensions.DependencyInjection": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "MCPrg7v3QgNMr0vX4vzRXvkNGgLg8vKWX0nKCWUxu2uPyMsaRgiRc1tHBnbTcfJMhMKj2slE/j2M9oGkd25DNw==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0" - } - }, - "Microsoft.Extensions.DependencyInjection.Abstractions": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "+6f2qv2a3dLwd5w6JanPIPs47CxRbnk+ZocMJUhv9NxP88VlOcJYZs9jY+MYSjxvady08bUZn6qgiNh7DadGgg==" - }, - "Microsoft.Extensions.Logging": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "crjWyORoug0kK7RSNJBTeSE6VX8IQgLf3nUpTB9m62bPXp/tzbnOsnbe8TXEG0AASNaKZddnpHKw7fET8E++Pg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection": "9.0.0", - "Microsoft.Extensions.Logging.Abstractions": "9.0.0", - "Microsoft.Extensions.Options": "9.0.0" - } - }, - "Microsoft.Extensions.Logging.Abstractions": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "g0UfujELzlLbHoVG8kPKVBaW470Ewi+jnptGS9KUi6jcb+k2StujtK3m26DFSGGwQ/+bVgZfsWqNzlP6YOejvw==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0" - } - }, - "Microsoft.Extensions.Options": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "y2146b3jrPI3Q0lokKXdKLpmXqakYbDIPDV6r3M8SqvSf45WwOTzkyfDpxnZXJsJQEpAsAqjUq5Pu8RCJMjubg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", - "Microsoft.Extensions.Primitives": "9.0.0" - } - }, - "Microsoft.Extensions.Primitives": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "N3qEBzmLMYiASUlKxxFIISP4AiwuPTHF5uCh+2CWSwwzAJiIYx0kBJsS30cp1nvhSySFAVi30jecD307jV+8Kg==" - }, - "Microsoft.NET.StringTools": { - "type": "Transitive", - "resolved": "17.14.28", - "contentHash": "DMIeWDlxe0Wz0DIhJZ2FMoGQAN2yrGZOi5jjFhRYHWR5ONd0CS6IpAHlRnA7uA/5BF+BADvgsETxW2XrPiFc1A==" - }, - "Mono.Cecil": { - "type": "Transitive", - "resolved": "0.11.6", - "contentHash": "f33RkDtZO8VlGXCtmQIviOtxgnUdym9xx/b1p9h91CRGOsJFxCFOFK1FDbVt1OCf1aWwYejUFa2MOQyFWTFjbA==" - }, - "MonoMod.Backports": { - "type": "Transitive", - "resolved": "1.1.2", - "contentHash": "baYlNy8n8kmaNhNvqmZ/dIPOeO1r9//dG1i2WbunMWtWZ2EKtIgmXaS+ZzphzTsikkGnoD4Jwr5g0TVdpDjgpw==", - "dependencies": { - "MonoMod.ILHelpers": "1.1.0" - } - }, - "MonoMod.Core": { - "type": "Transitive", - "resolved": "1.3.1", - "contentHash": "AE78s2Iv74mFfkbH+iUAGmVpf+zkMlJYOtQPVVuRN4Eorcy7Dm4wps1X/CVp4p6a7MnbCKOuQz9OeVC/xJN6+Q==", - "dependencies": { - "Mono.Cecil": "0.11.6", - "MonoMod.Backports": "1.1.2", - "MonoMod.ILHelpers": "1.1.0", - "MonoMod.Utils": "25.0.9" - } - }, - "MonoMod.ILHelpers": { - "type": "Transitive", - "resolved": "1.1.0", - "contentHash": "L2FWjhTrv7tcIxshfZ+M3OcaNr4cNw0IwiVZEgwqRnZ5QAN3+RrNJ8ZwCzwXUWyPDqooJxMcjjg8PsSYUiNBjQ==" - }, - "MonoMod.Utils": { - "type": "Transitive", - "resolved": "25.0.9", - "contentHash": "KPZ/VAr4zwT12/OJPZF2uazc9M/tRjiizeErq4m5Ie4EA6XnreakLfs8RlvxszgwXL7FVGnkg9JU5tKtQRPGMA==", - "dependencies": { - "Mono.Cecil": "0.11.6", - "MonoMod.Backports": "1.1.2", - "MonoMod.ILHelpers": "1.1.0" - } - }, - "Newtonsoft.Json": { - "type": "Transitive", - "resolved": "13.0.3", - "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" - }, - "StyleCop.Analyzers.Unstable": { - "type": "Transitive", - "resolved": "1.2.0.556", - "contentHash": "zvn9Mqs/ox/83cpYPignI8hJEM2A93s2HkHs8HYMOAQW0PkampyoErAiIyKxgTLqbbad29HX/shv/6LGSjPJNQ==" - }, - "System.CodeDom": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "oTE5IfuMoET8yaZP/vdvy9xO47guAv/rOhe4DODuFBN3ySprcQOlXqO3j+e/H/YpKKR5sglrxRaZ2HYOhNJrqA==" - }, - "System.Collections.Immutable": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "QhkXUl2gNrQtvPmtBTQHb0YsUrDiDQ2QS09YbtTTiSjGcf7NBqtYbrG/BE06zcBPCKEwQGzIv13IVdXNOSub2w==" - }, - "System.Composition": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "3Djj70fFTraOarSKmRnmRy/zm4YurICm+kiCtI0dYRqGJnLX6nJ+G3WYuFJ173cAPax/gh96REcbNiVqcrypFQ==", - "dependencies": { - "System.Composition.AttributedModel": "9.0.0", - "System.Composition.Convention": "9.0.0", - "System.Composition.Hosting": "9.0.0", - "System.Composition.Runtime": "9.0.0", - "System.Composition.TypedParts": "9.0.0" - } - }, - "System.Composition.AttributedModel": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "iri00l/zIX9g4lHMY+Nz0qV1n40+jFYAmgsaiNn16xvt2RDwlqByNG4wgblagnDYxm3YSQQ0jLlC/7Xlk9CzyA==" - }, - "System.Composition.Convention": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "+vuqVP6xpi582XIjJi6OCsIxuoTZfR0M7WWufk3uGDeCl3wGW6KnpylUJ3iiXdPByPE0vR5TjJgR6hDLez4FQg==", - "dependencies": { - "System.Composition.AttributedModel": "9.0.0" - } - }, - "System.Composition.Hosting": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "OFqSeFeJYr7kHxDfaViGM1ymk7d4JxK//VSoNF9Ux0gpqkLsauDZpu89kTHHNdCWfSljbFcvAafGyBoY094btQ==", - "dependencies": { - "System.Composition.Runtime": "9.0.0" - } - }, - "System.Composition.Runtime": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "w1HOlQY1zsOWYussjFGZCEYF2UZXgvoYnS94NIu2CBnAGMbXFAX8PY8c92KwUItPmowal68jnVLBCzdrWLeEKA==" - }, - "System.Composition.TypedParts": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "aRZlojCCGEHDKqh43jaDgaVpYETsgd7Nx4g1zwLKMtv4iTo0627715ajEFNpEEBTgLmvZuv8K0EVxc3sM4NWJA==", - "dependencies": { - "System.Composition.AttributedModel": "9.0.0", - "System.Composition.Hosting": "9.0.0", - "System.Composition.Runtime": "9.0.0" - } - }, - "System.Configuration.ConfigurationManager": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "PdkuMrwDhXoKFo/JxISIi9E8L+QGn9Iquj2OKDWHB6Y/HnUOuBouF7uS3R4Hw3FoNmwwMo6hWgazQdyHIIs27A==", - "dependencies": { - "System.Diagnostics.EventLog": "9.0.0", - "System.Security.Cryptography.ProtectedData": "9.0.0" - } - }, - "System.Diagnostics.EventLog": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "qd01+AqPhbAG14KtdtIqFk+cxHQFZ/oqRSCoxU1F+Q6Kv0cl726sl7RzU9yLFGd4BUOKdN4XojXF0pQf/R6YeA==" - }, - "System.Formats.Nrbf": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "F/6tNE+ckmdFeSQAyQo26bQOqfPFKEfZcuqnp4kBE6/7jP26diP+QTHCJJ6vpEfaY6bLy+hBLiIQUSxSmNwLkA==" - }, - "System.IO.Pipelines": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "eA3cinogwaNB4jdjQHOP3Z3EuyiDII7MT35jgtnsA4vkn0LUrrSHsU0nzHTzFzmaFYeKV7MYyMxOocFzsBHpTw==" - }, - "System.Reflection.Metadata": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "ANiqLu3DxW9kol/hMmTWbt3414t9ftdIuiIU7j80okq2YzAueo120M442xk1kDJWtmZTqWQn7wHDvMRipVOEOQ==" - }, - "System.Reflection.MetadataLoadContext": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "nGdCUVhEQ9/CWYqgaibYEDwIJjokgIinQhCnpmtZfSXdMS6ysLZ8p9xvcJ8VPx6Xpv5OsLIUrho4B9FN+VV/tw==" - }, - "System.Resources.Extensions": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "tvhuT1D2OwPROdL1kRWtaTJliQo0WdyhvwDpd8RM997G7m3Hya5nhbYhNTS75x6Vu+ypSOgL5qxDCn8IROtCxw==", - "dependencies": { - "System.Formats.Nrbf": "9.0.0" - } - }, - "System.Security.Cryptography.Pkcs": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "8tluJF8w9si+2yoHeL8rgVJS6lKvWomTDC8px65Z8MCzzdME5eaPtEQf4OfVGrAxB5fW93ncucy1+221O9EQaw==" - }, - "System.Security.Cryptography.ProtectedData": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "CJW+x/F6fmRQ7N6K8paasTw9PDZp4t7G76UjGNlSDgoHPF0h08vTzLYbLZpOLEJSg35d5wy2jCXGo84EN05DpQ==" - }, - "System.Security.Cryptography.Xml": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "GQZn5wFd+pyOfwWaCbqxG7trQ5ox01oR8kYgWflgtux4HiUNihGEgG2TktRWyH+9bw7NoEju1D41H/upwQeFQw==", - "dependencies": { - "System.Security.Cryptography.Pkcs": "9.0.0" - } - }, - "System.Security.Permissions": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "H2VFD4SFVxieywNxn9/epb63/IOcPPfA0WOtfkljzNfu7GCcHIBQNuwP6zGCEIi7Ci/oj8aLPUNK9sYImMFf4Q==", - "dependencies": { - "System.Windows.Extensions": "9.0.0" - } - }, - "System.Text.Json": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "js7+qAu/9mQvnhA4EfGMZNEzXtJCDxgkgj8ohuxq/Qxv+R56G+ljefhiJHOxTNiw54q8vmABCWUwkMulNdlZ4A==" - }, - "System.Threading.Channels": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "qmeeYNROMsONF6ndEZcIQ+VxR4Q/TX/7uIVLJqtwIWL7dDWeh0l1UIqgo4wYyjG//5lUNhwkLDSFl+pAWO6oiA==" - }, - "System.Threading.Tasks.Dataflow": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "S+y+QuBJNcqOvoFK+rFcZZuQDlD2E4lImKW9/g3E0l7YT2uo4oin9amAn398eGt/xFBYNNSt5O77Dbc38XGfBw==" - }, - "System.Windows.Extensions": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "U9msthvnH2Fsw7xwAvIhNHOdnIjOQTwOc8Vd0oGOsiRcGMGoBFlUD6qtYawRUoQdKH9ysxesZ9juFElt1Jw/7A==" - }, - "compatunbreaker": { - "type": "Project", - "dependencies": { - "AsmResolver.DotNet": "[6.0.0-dev, )", - "CompatUnbreaker.Attributes": "[1.0.0-dev, )", - "Meziantou.Analyzer": "[2.0.220, )", - "PolySharp": "[1.15.0, )", - "StyleCop.Analyzers": "[1.2.0-beta.556, )" - } - }, - "compatunbreaker.attributes": { - "type": "Project", - "dependencies": { - "Meziantou.Analyzer": "[2.0.220, )", - "PolySharp": "[1.15.0, )", - "StyleCop.Analyzers": "[1.2.0-beta.556, )" - } - } - } - } -} \ No newline at end of file diff --git a/src/build/CompatUnbreaker/CompatUnbreaker.sln b/src/build/CompatUnbreaker/CompatUnbreaker.sln deleted file mode 100644 index 88ced00..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker.sln +++ /dev/null @@ -1,52 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompatUnbreaker", "CompatUnbreaker\CompatUnbreaker.csproj", "{106A0BD3-DF0A-4DEE-81FD-3BE81A4B342D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompatUnbreaker.Tool", "CompatUnbreaker.Tool\CompatUnbreaker.Tool.csproj", "{E5EC6CFD-819E-46C0-9C87-9EE1E4B802BE}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompatUnbreaker.Attributes", "CompatUnbreaker.Attributes\CompatUnbreaker.Attributes.csproj", "{42F48A69-3114-4ADE-9892-41ABA5435FCB}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Playground", "Playground", "{9B9BB47F-F286-400F-8535-5B493FC4D738}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlaygroundLibrary.V1", "PlaygroundLibrary\PlaygroundLibrary.V1.csproj", "{6EF8D01D-B0C6-4B06-BD7E-C450513E8B7A}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlaygroundLibrary.V2", "PlaygroundLibrary\PlaygroundLibrary.V2.csproj", "{1B3331D2-62F5-4351-9715-5D2AC1DAF85F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompatUnbreaker.Tests", "CompatUnbreaker.Tests\CompatUnbreaker.Tests.csproj", "{9F2362A1-2505-4128-895B-97EC55E81758}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {106A0BD3-DF0A-4DEE-81FD-3BE81A4B342D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {106A0BD3-DF0A-4DEE-81FD-3BE81A4B342D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {106A0BD3-DF0A-4DEE-81FD-3BE81A4B342D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {106A0BD3-DF0A-4DEE-81FD-3BE81A4B342D}.Release|Any CPU.Build.0 = Release|Any CPU - {E5EC6CFD-819E-46C0-9C87-9EE1E4B802BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E5EC6CFD-819E-46C0-9C87-9EE1E4B802BE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E5EC6CFD-819E-46C0-9C87-9EE1E4B802BE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E5EC6CFD-819E-46C0-9C87-9EE1E4B802BE}.Release|Any CPU.Build.0 = Release|Any CPU - {42F48A69-3114-4ADE-9892-41ABA5435FCB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {42F48A69-3114-4ADE-9892-41ABA5435FCB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {42F48A69-3114-4ADE-9892-41ABA5435FCB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {42F48A69-3114-4ADE-9892-41ABA5435FCB}.Release|Any CPU.Build.0 = Release|Any CPU - {6EF8D01D-B0C6-4B06-BD7E-C450513E8B7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6EF8D01D-B0C6-4B06-BD7E-C450513E8B7A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6EF8D01D-B0C6-4B06-BD7E-C450513E8B7A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6EF8D01D-B0C6-4B06-BD7E-C450513E8B7A}.Release|Any CPU.Build.0 = Release|Any CPU - {1B3331D2-62F5-4351-9715-5D2AC1DAF85F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1B3331D2-62F5-4351-9715-5D2AC1DAF85F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1B3331D2-62F5-4351-9715-5D2AC1DAF85F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1B3331D2-62F5-4351-9715-5D2AC1DAF85F}.Release|Any CPU.Build.0 = Release|Any CPU - {9F2362A1-2505-4128-895B-97EC55E81758}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9F2362A1-2505-4128-895B-97EC55E81758}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9F2362A1-2505-4128-895B-97EC55E81758}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9F2362A1-2505-4128-895B-97EC55E81758}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {6EF8D01D-B0C6-4B06-BD7E-C450513E8B7A} = {9B9BB47F-F286-400F-8535-5B493FC4D738} - {1B3331D2-62F5-4351-9715-5D2AC1DAF85F} = {9B9BB47F-F286-400F-8535-5B493FC4D738} - EndGlobalSection -EndGlobal diff --git a/src/build/CompatUnbreaker/CompatUnbreaker/CompatUnbreaker.csproj b/src/build/CompatUnbreaker/CompatUnbreaker/CompatUnbreaker.csproj deleted file mode 100644 index 8e792fe..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker/CompatUnbreaker.csproj +++ /dev/null @@ -1,25 +0,0 @@ - - - true - - net9.0;net6.0 - - - - - - - - - - - - - - - - - - - - diff --git a/src/build/CompatUnbreaker/CompatUnbreaker/Models/Attributes/AttributeDescription.cs b/src/build/CompatUnbreaker/CompatUnbreaker/Models/Attributes/AttributeDescription.cs deleted file mode 100644 index c283c95..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker/Models/Attributes/AttributeDescription.cs +++ /dev/null @@ -1,50 +0,0 @@ -using AsmResolver; -using AsmResolver.DotNet; -using AsmResolver.DotNet.Signatures; -using AsmResolver.PE.DotNet.Metadata.Tables; - -namespace CompatUnbreaker.Models.Attributes; - -internal static class AttributeDescription -{ - private static Utf8String CompatUnbreakerAttributesNamespace { get; } = "CompatUnbreaker.Attributes"u8; - - public static MarkerAttributeDescription UnbreakerConstructorAttribute { get; } = new(CompatUnbreakerAttributesNamespace, "UnbreakerConstructorAttribute"u8); - public static SingleValueAttributeDescription UnbreakerExtensionAttribute { get; } = new(CompatUnbreakerAttributesNamespace, "UnbreakerExtensionAttribute"u8); - public static MarkerAttributeDescription UnbreakerExtensionsAttribute { get; } = new(CompatUnbreakerAttributesNamespace, "UnbreakerExtensionsAttribute"u8); - - public static MarkerAttributeDescription UnbreakerFieldAttribute { get; } = new(CompatUnbreakerAttributesNamespace, "UnbreakerFieldAttribute"u8); - - public static UnbreakerRenameAttributeDescription UnbreakerRenameAttribute { get; } = new(CompatUnbreakerAttributesNamespace, "UnbreakerRenameAttribute"u8); - public static SingleValueAttributeDescription UnbreakerReplaceAttribute { get; } = new(CompatUnbreakerAttributesNamespace, "UnbreakerReplaceAttribute"u8); - public static SingleValueAttributeDescription UnbreakerShimAttribute { get; } = new(CompatUnbreakerAttributesNamespace, "UnbreakerShimAttribute"u8); -} - -internal abstract class AttributeDescription(Utf8String @namespace, Utf8String name) -{ - public Utf8String Namespace { get; } = @namespace; - - public Utf8String Name { get; } = name; - - public abstract TData CreateData(CustomAttribute customAttribute); -} - -internal sealed class MarkerAttributeDescription(Utf8String @namespace, Utf8String name) : AttributeDescription(@namespace, name) -{ - public override CustomAttribute CreateData(CustomAttribute customAttribute) => customAttribute; -} - -internal sealed class SingleValueAttributeDescription(Utf8String @namespace, Utf8String name) : AttributeDescription(@namespace, name) -{ - public override T CreateData(CustomAttribute customAttribute) - { - ArgumentNullException.ThrowIfNull(customAttribute.Signature); - - var argument = customAttribute.Signature.FixedArguments.Single(); - var value = argument.ArgumentType.ElementType == ElementType.SzArray - ? argument.Elements - : argument.Element; - - return (T) value!; - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker/Models/Attributes/AttributeDescriptionExtensions.cs b/src/build/CompatUnbreaker/CompatUnbreaker/Models/Attributes/AttributeDescriptionExtensions.cs deleted file mode 100644 index 9f4dba2..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker/Models/Attributes/AttributeDescriptionExtensions.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using AsmResolver.DotNet; - -namespace CompatUnbreaker.Models.Attributes; - -internal static class AttributeDescriptionExtensions -{ - public static IEnumerable Find(this IHasCustomAttribute provider, AttributeDescription attributeDescription) - { - foreach (var attribute in provider.CustomAttributes) - { - var declaringType = attribute.Type; - if (declaringType is null) - continue; - - if (declaringType.Namespace == attributeDescription.Namespace && declaringType.Name == attributeDescription.Name) - { - yield return attributeDescription.CreateData(attribute); - } - } - } - - public static bool Has(this IHasCustomAttribute provider, AttributeDescription attributeDescription) - { - return provider.Find(attributeDescription).Any(); - } - - public static bool TryFindSingle(this IHasCustomAttribute provider, AttributeDescription attributeDescription, [MaybeNullWhen(false)] out TData result) - { - return provider.Find(attributeDescription).TryGetSingle(out result); - } - - private static bool TryGetSingle(this IEnumerable source, [MaybeNullWhen(false)] out TSource result) - { - ArgumentNullException.ThrowIfNull(source); - - using (var e = source.GetEnumerator()) - { - if (!e.MoveNext()) - { - result = default; - return false; - } - - result = e.Current; - if (!e.MoveNext()) - { - return true; - } - } - - throw new InvalidOperationException("Sequence contains more than one element"); - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker/Models/Attributes/UnbreakerRenameAttributeDescription.cs b/src/build/CompatUnbreaker/CompatUnbreaker/Models/Attributes/UnbreakerRenameAttributeDescription.cs deleted file mode 100644 index 6c5e7d4..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker/Models/Attributes/UnbreakerRenameAttributeDescription.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.Diagnostics; -using AsmResolver; -using AsmResolver.DotNet; -using AsmResolver.DotNet.Signatures; -using CompatUnbreaker.Utilities.AsmResolver; - -namespace CompatUnbreaker.Models.Attributes; - -internal sealed class UnbreakerRenameAttributeDescription(Utf8String @namespace, Utf8String name) - : AttributeDescription(@namespace, name) -{ - public override RenameData CreateData(CustomAttribute customAttribute) - { - ArgumentNullException.ThrowIfNull(customAttribute.Constructor); - ArgumentNullException.ThrowIfNull(customAttribute.Constructor.ContextModule); - ArgumentNullException.ThrowIfNull(customAttribute.Signature); - - var typeFactory = customAttribute.Constructor.ContextModule.CorLibTypeFactory; - - if (CheckSignature(typeFactory.Type(), typeFactory.String)) - { - var arguments = customAttribute.Signature.FixedArguments; - Debug.Assert(arguments.Count == 2); - - return new RenameData.TypeRename( - (TypeSignature) arguments[0].Element!, - (Utf8String) arguments[1].Element! - ); - } - - if (CheckSignature(typeFactory.Type(), typeFactory.String, typeFactory.String)) - { - var arguments = customAttribute.Signature.FixedArguments; - Debug.Assert(arguments.Count == 3); - - return new RenameData.MemberRename( - (TypeSignature) arguments[0].Element!, - (Utf8String) arguments[1].Element!, - (Utf8String) arguments[2].Element! - ); - } - - throw new ArgumentException($"Invalid signature for '{customAttribute}'.", nameof(customAttribute)); - - bool CheckSignature(params ITypeDescriptor[] parameterTypes) - { - return SignatureComparer.Default.Equals( - customAttribute.Constructor?.Signature?.ParameterTypes, - parameterTypes.Select(t => t.ToTypeSignature()) - ); - } - } -} - -internal abstract record RenameData -{ - public sealed record NamespaceRename(Utf8String NamespaceName, Utf8String NewNamespaceName) : RenameData; - - public sealed record TypeRename(TypeSignature Type, Utf8String NewTypeName) : RenameData; - - public sealed record MemberRename(TypeSignature Type, Utf8String MemberName, Utf8String NewMemberName) : RenameData; -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker/Models/ShimModel.cs b/src/build/CompatUnbreaker/CompatUnbreaker/Models/ShimModel.cs deleted file mode 100644 index bf5aa2f..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker/Models/ShimModel.cs +++ /dev/null @@ -1,310 +0,0 @@ -using AsmResolver.DotNet; -using AsmResolver.DotNet.Signatures; -using CompatUnbreaker.Attributes; -using CompatUnbreaker.Models.Attributes; -using CompatUnbreaker.Utilities.AsmResolver; - -namespace CompatUnbreaker.Models; - -internal sealed class ShimModel -{ - public required AssemblyDefinition ShimAssembly { get; init; } - public required AssemblyDefinition TargetAssembly { get; init; } - - public required RenameData[] Renames { get; init; } - public required ShimTypeModel[] AllTypes { get; init; } - public required Dictionary ExtensionImplementations { get; init; } - - public static ShimModel From(AssemblyDefinition shimAssembly) - { - var shimModule = shimAssembly.Modules.Single(); - - var targetAssemblyName = shimAssembly.Find(AttributeDescription.UnbreakerShimAttribute).SingleOrDefault() - ?? throw new ArgumentException("Provided shim assembly doesn't have the required UnbreakerShimAttribute.", nameof(shimAssembly)); - - var targetAssemblyReference = shimModule.AssemblyReferences.SingleOrDefault(a => a.Name == targetAssemblyName) - ?? new AssemblyReference(targetAssemblyName, new Version()); - - var targetAssembly = shimModule.MetadataResolver.AssemblyResolver.Resolve(targetAssemblyReference) - ?? throw new ArgumentException($"Could not resolve target assembly '{targetAssemblyReference}'."); - - var targetModule = targetAssembly.Modules.Single(); - - var renames = shimAssembly.Find(AttributeDescription.UnbreakerRenameAttribute).ToArray(); - - var extensionImplementations = new Dictionary(SignatureComparer.Default); - - var typeModels = new List(); - - var queue = new Queue<(ShimTypeModel? DeclaringType, TypeDefinition Type)>( - shimModule.TopLevelTypes.Select(t => ((ShimTypeModel?) null, t)) - ); - - while (queue.Count > 0) - { - var (declaringType, shimType) = queue.Dequeue(); - - var hasExtensionsAttribute = shimType.Has(AttributeDescription.UnbreakerExtensionsAttribute); - if (hasExtensionsAttribute) - { - foreach (var shimNestedType in shimType.NestedTypes) - { - queue.Enqueue((null, shimNestedType)); - } - - continue; - } - - ShimTypeKind kind; - ITypeDefOrRef targetReference; - TypeSignature? extensionParameter = null; - - if (shimType.TryGetExtensionMarkerMethod() is { } markerMethod && shimType.DeclaringType.Has(AttributeDescription.UnbreakerExtensionsAttribute)) - { - kind = ShimTypeKind.NativeExtension; - extensionParameter = markerMethod.Signature!.ParameterTypes.Single(); - targetReference = extensionParameter.GetUnderlyingTypeDefOrRef(); - // TODO validate target type generics match shim type generics 1 to 1 - } - else if (shimType.TryFindSingle(AttributeDescription.UnbreakerExtensionAttribute, out var extensionAttributeTarget)) - { - kind = ShimTypeKind.UnbreakerExtension; - targetReference = ((TypeDefOrRefSignature) extensionAttributeTarget!).Type; - } - else if (shimType.TryFindSingle(AttributeDescription.UnbreakerReplaceAttribute, out var replaceAttributeTarget)) - { - kind = ShimTypeKind.Replace; - targetReference = ((TypeDefOrRefSignature) replaceAttributeTarget!).Type; - } - else - { - kind = ShimTypeKind.New; - - IResolutionScope scope = declaringType == null - ? targetModule - : declaringType.TargetDescriptor switch - { - TypeReference reference => reference, - TypeDefinition definition => definition.ToTypeReference(), - _ => throw new ArgumentOutOfRangeException(), - }; - - targetReference = new TypeReference(shimModule, scope, shimType.Namespace, shimType.Name); - } - - if (!shimType.IsVisibleOutsideOfAssembly()) - { - if (kind == ShimTypeKind.New) - { - continue; - } - - throw new InvalidOperationException($"Shim type '{shimType.FullName}' isn't public."); - } - - if (targetReference.Scope?.GetAssembly()?.Name != targetAssembly.Name) - { - throw new InvalidOperationException($"Shim type '{shimType.FullName}' targets type '{targetReference}', which is not in the target assembly."); - } - - var targetType = targetReference.Resolve(); - if (targetType != null && !targetType.IsVisibleOutsideOfAssembly()) - { - targetType = null; - } - - if (kind == ShimTypeKind.New && targetType != null) - { - throw new InvalidOperationException($"Shim type '{shimType.FullName}' conflicts with target type and doesn't specify the {nameof(UnbreakerReplaceAttribute)}."); - } - - if (kind != ShimTypeKind.New && targetType == null) - { - throw new InvalidOperationException($"Shim type '{shimType.FullName}' target type '{targetReference}' could not be resolved."); - } - - if (kind == ShimTypeKind.UnbreakerExtension && !shimType.IsStatic()) - { - throw new InvalidOperationException($"Extension shim type '{shimType.FullName}' is not static."); - } - - var members = new List(); - - var ignoredMethods = new HashSet(); - - foreach (var shimProperty in shimType.Properties) - { - if (!shimProperty.IsVisibleOutsideOfAssembly()) continue; - - var hasFieldAttribute = shimProperty.Has(AttributeDescription.UnbreakerFieldAttribute); - - if (hasFieldAttribute) - { - var fieldType = shimProperty.Signature!.ReturnType; - members.Add(new ShimFieldModel.FromProperty - { - TargetDescriptor = new MemberReference(targetReference, shimProperty.Name, new FieldSignature(fieldType)), - Definition = shimProperty, - }); - - if (shimProperty.GetMethod != null) ignoredMethods.Add(shimProperty.GetMethod); - if (shimProperty.SetMethod != null) ignoredMethods.Add(shimProperty.SetMethod); - } - else - { - members.Add(new ShimPropertyModel - { - Definition = shimProperty, - }); - } - } - - foreach (var shimEvent in shimType.Events) - { - if (!shimEvent.IsVisibleOutsideOfAssembly()) continue; - - members.Add(new ShimEventModel - { - Definition = shimEvent, - }); - } - - foreach (var shimMethod in shimType.Methods) - { - if (!shimMethod.IsVisibleOutsideOfAssembly()) continue; - - if (extensionParameter != null) - { - var implementationMethod = shimType.FindCorrespondingExtensionImplementationMethod(shimMethod, extensionParameter) - ?? throw new InvalidOperationException($"Couldn't find corresponding implementation method for {shimMethod}."); - - extensionImplementations.Add(shimMethod, implementationMethod); - } - - if (ignoredMethods.Contains(shimMethod)) continue; - - var isConstructor = shimMethod.Has(AttributeDescription.UnbreakerConstructorAttribute); - - members.Add(new ShimMethodModel - { - IsUnbreakerConstructor = isConstructor, - TargetDescriptor = isConstructor - ? new MemberReference(targetReference, ".ctor"u8, MethodSignature.CreateInstance(targetReference.ContextModule.CorLibTypeFactory.Void, shimMethod.Signature.ParameterTypes)) - : new MemberReference(targetReference, shimMethod.Name, shimMethod.Signature), - Definition = shimMethod, - }); - } - - foreach (var shimField in shimType.Fields) - { - if (!shimField.IsVisibleOutsideOfAssembly()) continue; - - members.Add(new ShimFieldModel.FromField - { - TargetDescriptor = new MemberReference(targetReference, shimField.Name, shimField.Signature), - Definition = shimField, - }); - } - - var typeModel = new ShimTypeModel - { - Kind = kind, - DeclaringType = declaringType, - TargetDescriptor = targetReference, - Members = members.ToArray(), - Definition = shimType, - }; - - typeModels.Add(typeModel); - - foreach (var shimNestedType in shimType.NestedTypes) - { - queue.Enqueue((typeModel, shimNestedType)); - } - } - - return new ShimModel - { - ShimAssembly = shimAssembly, - TargetAssembly = targetAssembly, - Renames = renames, - AllTypes = typeModels.ToArray(), - ExtensionImplementations = extensionImplementations, - }; - } -} - -internal enum ShimTypeKind -{ - New, - Replace, - NativeExtension, - UnbreakerExtension, -} - -internal interface IShimMemberModel -{ - IMemberDescriptor TargetDescriptor { get; } -} - -internal sealed class ShimTypeModel : IShimMemberModel -{ - public required ShimTypeKind Kind { get; init; } - - public required ShimTypeModel? DeclaringType { get; init; } - public required ITypeDefOrRef TargetDescriptor { get; init; } - IMemberDescriptor IShimMemberModel.TargetDescriptor => TargetDescriptor; - - public required IShimMemberModel[] Members { get; init; } - - public IEnumerable Methods => Members.OfType(); - public IEnumerable Fields => Members.OfType(); - public IEnumerable Properties => Members.OfType(); - public IEnumerable Events => Members.OfType(); - - public required TypeDefinition Definition { get; init; } - - public override string ToString() - { - return $"{Kind} : {TargetDescriptor}"; - } -} - -internal sealed class ShimMethodModel : IShimMemberModel -{ - public required MemberReference TargetDescriptor { get; init; } - IMemberDescriptor IShimMemberModel.TargetDescriptor => TargetDescriptor; - - public required bool IsUnbreakerConstructor { get; init; } - public required MethodDefinition Definition { get; init; } -} - -internal abstract class ShimFieldModel : IShimMemberModel -{ - public required MemberReference TargetDescriptor { get; init; } - IMemberDescriptor IShimMemberModel.TargetDescriptor => TargetDescriptor; - - internal sealed class FromField : ShimFieldModel - { - public required FieldDefinition Definition { get; init; } - } - - internal sealed class FromProperty : ShimFieldModel - { - public required PropertyDefinition Definition { get; init; } - } -} - -internal sealed class ShimPropertyModel : IShimMemberModel -{ - IMemberDescriptor IShimMemberModel.TargetDescriptor => throw new NotSupportedException(); - - public required PropertyDefinition Definition { get; init; } -} - -internal sealed class ShimEventModel : IShimMemberModel -{ - IMemberDescriptor IShimMemberModel.TargetDescriptor => throw new NotSupportedException(); - - public required EventDefinition Definition { get; init; } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker/Processors/Abstractions/IConsumerProcessor.cs b/src/build/CompatUnbreaker/CompatUnbreaker/Processors/Abstractions/IConsumerProcessor.cs deleted file mode 100644 index a24b769..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker/Processors/Abstractions/IConsumerProcessor.cs +++ /dev/null @@ -1,8 +0,0 @@ -using AsmResolver.DotNet; - -namespace CompatUnbreaker.Processors.Abstractions; - -internal interface IConsumerProcessor -{ - void Process(ProcessorContext context, AssemblyDefinition consumerAssembly); -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker/Processors/Abstractions/IReferenceProcessor.cs b/src/build/CompatUnbreaker/CompatUnbreaker/Processors/Abstractions/IReferenceProcessor.cs deleted file mode 100644 index 2ad6f11..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker/Processors/Abstractions/IReferenceProcessor.cs +++ /dev/null @@ -1,8 +0,0 @@ -using AsmResolver.DotNet; - -namespace CompatUnbreaker.Processors.Abstractions; - -internal interface IReferenceProcessor -{ - void Process(ProcessorContext context, AssemblyDefinition referenceAssembly); -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker/Processors/Abstractions/ProcessorContext.cs b/src/build/CompatUnbreaker/CompatUnbreaker/Processors/Abstractions/ProcessorContext.cs deleted file mode 100644 index 904f1ee..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker/Processors/Abstractions/ProcessorContext.cs +++ /dev/null @@ -1,8 +0,0 @@ -using CompatUnbreaker.Models; - -namespace CompatUnbreaker.Processors.Abstractions; - -internal sealed class ProcessorContext -{ - public required ShimModel ShimModel { get; init; } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker/Processors/UnbreakerConsumerProcessor.cs b/src/build/CompatUnbreaker/CompatUnbreaker/Processors/UnbreakerConsumerProcessor.cs deleted file mode 100644 index 26d623d..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker/Processors/UnbreakerConsumerProcessor.cs +++ /dev/null @@ -1,389 +0,0 @@ -using System.Diagnostics; -using AsmResolver; -using AsmResolver.DotNet; -using AsmResolver.DotNet.Code.Cil; -using AsmResolver.DotNet.Signatures; -using AsmResolver.PE.DotNet.Cil; -using CompatUnbreaker.Models; -using CompatUnbreaker.Models.Attributes; -using CompatUnbreaker.Processors.Abstractions; -using CompatUnbreaker.Utilities.AsmResolver; - -namespace CompatUnbreaker.Processors; - -internal sealed class UnbreakerConsumerProcessor : IConsumerProcessor -{ - public void Process(ProcessorContext context, AssemblyDefinition consumerAssembly) - { - var shimModel = context.ShimModel; - var consumerModule = consumerAssembly.ManifestModule!; - - var memberRenames = shimModel.Renames.OfType() - .GroupBy(r => r.Type, SignatureComparer.Default) - .ToDictionary( - x => x.Key.ToTypeDefOrRef(), - g => g.ToDictionary(r => r.MemberName, r => r.NewMemberName), - SignatureComparer.Default - ); - - var shimMembers = new Dictionary(SignatureComparer.VersionAgnostic); - - foreach (var type in shimModel.AllTypes) - { - if (type.Kind is ShimTypeKind.New or ShimTypeKind.Replace) - { - shimMembers.Add(type.TargetDescriptor, type); - } - - foreach (var member in type.Members) - { - if (member is ShimPropertyModel or ShimEventModel) continue; - shimMembers.Add(member.TargetDescriptor, member); - } - } - - var shimmingImporter = new ShimmingReferenceImporter(consumerModule, shimModel, shimMembers, memberRenames); - var memberShimmer = new MemberShimmer(shimmingImporter, shimMembers, memberRenames); - - foreach (var type in consumerModule.GetAllTypes()) - { - memberShimmer.Visit(type); - } - } -} - -internal sealed class MemberShimmer( - ShimmingReferenceImporter importer, - Dictionary shimMembers, - Dictionary> memberRenames -) : ShimmerImporter(importer) -{ - protected override void VisitMethod(MethodDefinition method) - { - if (method is { IsVirtual: true, IsNewSlot: false }) - { - var baseType = method.DeclaringType!; - - while (true) - { - baseType = baseType.BaseType?.Resolve(); - if (baseType == null) break; - - if (memberRenames.TryGetValue(baseType, out var renames)) - { - if (renames.TryGetValue(method.Name, out var newName)) - { - method.Name = newName; - } - } - } - } - - base.VisitMethod(method); - } - - protected override void VisitCilInstruction(CilMethodBody body, int index, CilInstruction instruction) - { - var instructions = body.Instructions; - - if (instruction.OpCode.OperandType is CilOperandType.InlineField && instruction.Operand is IFieldDescriptor field) - { - var opCode = instruction.OpCode.Code; - - if (shimMembers.TryGetValue(field, out var shimMember)) - { - var shimField = (ShimFieldModel) shimMember; - - switch (shimField) - { - case ShimFieldModel.FromField fromField: - { - instruction.Operand = fromField.Definition.ImportWith(importer); - break; - } - - case ShimFieldModel.FromProperty fromProperty: - { - var accessor = opCode switch - { - CilCode.Ldfld or CilCode.Ldsfld or CilCode.Ldflda or CilCode.Ldsflda => fromProperty.Definition.GetMethod, - CilCode.Stfld or CilCode.Stsfld => fromProperty.Definition.SetMethod, - _ => throw new ArgumentOutOfRangeException(), - }; - - instruction.ReplaceWith(CilOpCodes.Call, accessor.ImportWith(importer)); - - // Create a temporary local for getting dummy field address - if (opCode is CilCode.Ldflda or CilCode.Ldsflda) - { - var temporaryLocal = new CilLocalVariable(field.Signature.FieldType.ImportWith(importer)); - body.LocalVariables.Add(temporaryLocal); - instructions.Insert(++index, CilOpCodes.Stloc, temporaryLocal); - instructions.Insert(++index, CilOpCodes.Ldloca, temporaryLocal); - - // TODO we could have a simple heuristic here that warns if the next instruction is not a call to a readonly method - } - - break; - } - - default: - throw new ArgumentOutOfRangeException(); - } - - return; - } - } - - base.VisitCilInstruction(body, index, instruction); - } -} - -internal sealed class ShimmingReferenceImporter( - ModuleDefinition module, - ShimModel shimModel, - Dictionary shimMembers, - Dictionary> memberRenames -) : ReferenceVisitor(module) -{ - public override IMethodDefOrRef ImportMethod(IMethodDefOrRef method) - { - ArgumentNullException.ThrowIfNull(method); - if (method.DeclaringType is null) - throw new ArgumentException("Cannot import a method that is not added to a type."); - if (method.Signature is null) - throw new ArgumentException("Cannot import a method that does not have a signature."); - - if (shimModel.ExtensionImplementations.TryGetValue(method, out var implementation)) - { - return ImportMethod(implementation); - } - - var signature = ImportMethodSignature(method.Signature); - - var name = method.Name; - ArgumentNullException.ThrowIfNull(name); - - if (shimMembers.TryGetValue(new MemberReference(method.DeclaringType, name, signature), out var shimMember)) - { - return ImportMethod(((ShimMethodModel) shimMember).Definition); - } - - if (memberRenames.TryGetValue(method.DeclaringType.ToTypeSignature().GetUnderlyingTypeDefOrRef(), out var renames) && - renames.TryGetValue(name, out var newName)) - { - name = newName; - } - - return new MemberReference(ImportType(method.DeclaringType), name, signature); - } - - public override MethodSpecification ImportMethod(MethodSpecification method) - { - if (shimModel.ExtensionImplementations.TryGetValue(method.Method, out var implementation)) - { - throw new NotImplementedException(); - return base.ImportMethod(method); - } - - return base.ImportMethod(method); - } - - protected override ITypeDefOrRef ImportType(TypeReference type) - { - if (shimMembers.TryGetValue(type, out var shimMember)) - { - return base.ImportType(((ShimTypeModel) shimMember).Definition); - } - - return base.ImportType(type); - } -} - -internal class ShimmerImporter(ReferenceImporter importer) -{ - public virtual void Visit(TypeDefinition type) - { - type.BaseType = type.BaseType?.ImportWith(importer); - - foreach (var implementation in type.Interfaces) - { - implementation.Interface = implementation.Interface?.ImportWith(importer); - VisitCustomAttributes(implementation); - } - - for (var i = 0; i < type.MethodImplementations.Count; i++) - { - var implementation = type.MethodImplementations[i]; - type.MethodImplementations[i] = new MethodImplementation( - implementation.Declaration?.ImportWith(importer), - implementation.Body?.ImportWith(importer) - ); - } - - VisitCustomAttributes(type); - VisitGenericParameters(type); - - foreach (var field in type.Fields) - { - field.Signature = field.Signature?.ImportWith(importer); - VisitCustomAttributes(field); - } - - foreach (var method in type.Methods) - { - VisitMethod(method); - } - - foreach (var property in type.Properties) - { - property.Signature = property.Signature?.ImportWith(importer); - } - - foreach (var @event in type.Events) - { - @event.EventType = @event.EventType?.ImportWith(importer); - } - } - - private void VisitCustomAttributes(IHasCustomAttribute provider) - { - foreach (var attribute in provider.CustomAttributes) - { - attribute.Constructor = (ICustomAttributeType?) attribute.Constructor?.ImportWith(importer); - - if (attribute.Signature is { } signature) - { - foreach (var argument in signature.FixedArguments) - { - VisitCustomAttributeArgument(argument); - } - - foreach (var argument in signature.NamedArguments) - { - VisitCustomAttributeArgument(argument.Argument); - } - } - } - } - - private void VisitCustomAttributeArgument(CustomAttributeArgument argument) - { - argument.ArgumentType = argument.ArgumentType.ImportWith(importer); - for (var i = 0; i < argument.Elements.Count; i++) - { - var element = argument.Elements[i]; - if (element is TypeSignature typeSignature) - { - argument.Elements[i] = typeSignature.ImportWith(importer); - } - } - } - - private void VisitGenericParameters(IHasGenericParameters provider) - { - foreach (var parameter in provider.GenericParameters) - { - foreach (var constraint in parameter.Constraints) - { - constraint.Constraint = constraint.Constraint?.ImportWith(importer); - VisitCustomAttributes(constraint); - } - - VisitCustomAttributes(parameter); - } - } - - protected virtual void VisitMethod(MethodDefinition method) - { - method.Signature = method.Signature?.ImportWith(importer); - - VisitCustomAttributes(method); - VisitGenericParameters(method); - - foreach (var parameterDefinition in method.ParameterDefinitions) - { - VisitCustomAttributes(parameterDefinition); - } - - if (method.CilMethodBody is { } body) - { - foreach (var localVariable in body.LocalVariables) - { - localVariable.VariableType = localVariable.VariableType.ImportWith(importer); - } - - foreach (var exceptionHandler in body.ExceptionHandlers) - { - exceptionHandler.ExceptionType = exceptionHandler.ExceptionType?.ImportWith(importer); - } - - var instructions = body.Instructions; - for (var i = 0; i < instructions.Count; i++) - { - VisitCilInstruction(body, i, instructions[i]); - } - } - } - - protected virtual void VisitCilInstruction(CilMethodBody body, int index, CilInstruction instruction) - { - switch (instruction.OpCode.OperandType) - { - case CilOperandType.InlineField when instruction.Operand is IFieldDescriptor field: - { - instruction.Operand = field.ImportWith(importer); - break; - } - - case CilOperandType.InlineMethod when instruction.Operand is IMethodDescriptor methodDescriptor: - { - var newMethodDescriptor = methodDescriptor.ImportWith(importer); - if (!SignatureComparer.Default.Equals(newMethodDescriptor, methodDescriptor)) - { - if (instruction.OpCode.Code == CilCode.Callvirt && !newMethodDescriptor.Signature!.HasThis) - { - instruction.OpCode = CilOpCodes.Call; - } - - if (instruction.OpCode.Code == CilCode.Newobj && newMethodDescriptor.Name != ".ctor") - { - instruction.OpCode = CilOpCodes.Call; - } - - instruction.Operand = newMethodDescriptor; - } - - break; - } - - case CilOperandType.InlineSig when instruction.Operand is StandAloneSignature standAlone: - { - instruction.Operand = new StandAloneSignature(standAlone.Signature switch - { - MethodSignature signature => signature.ImportWith(importer), - GenericInstanceMethodSignature signature => signature.ImportWith(importer), - _ => throw new ArgumentOutOfRangeException(), - }); - - break; - } - - case CilOperandType.InlineType when instruction.Operand is ITypeDefOrRef typeDescriptor: - { - instruction.Operand = typeDescriptor.ImportWith(importer); - break; - } - - case CilOperandType.InlineTok: - { - if (instruction.Operand is IImportable importable) - { - instruction.Operand = importable.ImportWith(importer); - } - - break; - } - } - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker/Processors/UnbreakerReferenceProcessor.cs b/src/build/CompatUnbreaker/CompatUnbreaker/Processors/UnbreakerReferenceProcessor.cs deleted file mode 100644 index 0b9d193..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker/Processors/UnbreakerReferenceProcessor.cs +++ /dev/null @@ -1,247 +0,0 @@ -using System.Diagnostics; -using AsmResolver.DotNet; -using AsmResolver.DotNet.Code.Cil; -using AsmResolver.DotNet.Signatures; -using AsmResolver.PE.DotNet.Cil; -using AsmResolver.PE.DotNet.Metadata.Tables; -using CompatUnbreaker.Models; -using CompatUnbreaker.Models.Attributes; -using CompatUnbreaker.Processors.Abstractions; -using CompatUnbreaker.Utilities.AsmResolver; - -namespace CompatUnbreaker.Processors; - -internal sealed class UnbreakerReferenceProcessor : IReferenceProcessor -{ - public void Process(ProcessorContext context, AssemblyDefinition referenceAssembly) - { - if (context.ShimModel.TargetAssembly != referenceAssembly) - { - throw new InvalidOperationException("Shim model target assembly does not match the reference assembly."); - } - - var referenceModule = referenceAssembly.ManifestModule!; - - var importer = new RedirectReferenceImporter(referenceModule); - importer.Assemblies.Add(context.ShimModel.ShimAssembly, context.ShimModel.TargetAssembly); - - foreach (var type in context.ShimModel.AllTypes) - { - if (type.Kind == ShimTypeKind.Replace) - { - importer.Types.Add(type.Definition, type.TargetDescriptor); - } - } - - var memberCloner = new MemberClonerLite(importer); - - foreach (var rename in context.ShimModel.Renames.OfType()) - { - var typeDefinition = rename.Type.Resolve(); - - // TODO other members - typeDefinition.Methods.Single(m => m.Name == rename.NewMemberName).Name = rename.MemberName; - } - - var targetTypes = new Dictionary(); - - foreach (var shimTypeModel in context.ShimModel.AllTypes) - { - var targetType = shimTypeModel.TargetDescriptor.Resolve(); - if (targetType != null && !targetType.IsVisibleOutsideOfAssembly()) - { - targetType = null; - } - - var shimType = shimTypeModel.Definition; - - if (shimTypeModel.Kind == ShimTypeKind.Replace && targetType != null) - { - if (targetType.DeclaringType is { } targetDeclaringType) - { - targetDeclaringType.NestedTypes.Remove(targetType); - } - else - { - referenceModule.TopLevelTypes.Remove(targetType); - } - - targetType = null; - } - - if (targetType == null) - { - targetType = memberCloner.CloneType(shimType); - targetType.Namespace = shimTypeModel.TargetDescriptor.Namespace; - targetType.Name = shimTypeModel.TargetDescriptor.Name; - targetTypes.Add(shimTypeModel, targetType); - - if (shimTypeModel.TargetDescriptor.DeclaringType is { } shimDeclaringTypeDescriptor) - { - if (shimTypeModel.DeclaringType != null && targetTypes.TryGetValue(shimTypeModel.DeclaringType, out var shimDeclaringType)) - { - Debug.Assert(SignatureComparer.Default.Equals(shimDeclaringType, shimDeclaringTypeDescriptor)); - } - else - { - shimDeclaringType = shimDeclaringTypeDescriptor.Resolve() - ?? throw new InvalidOperationException($"Could not resolve declaring type for {shimTypeModel}."); - } - - shimDeclaringType.NestedTypes.Add(targetType); - } - else - { - referenceModule.TopLevelTypes.Add(targetType); - } - } - - var clonedMethods = new Dictionary(); - - foreach (var methodModel in shimTypeModel.Methods) - { - var targetMethod = memberCloner.CloneMethod(methodModel.Definition); - targetMethod.Name = methodModel.TargetDescriptor.Name; - targetMethod.Signature = (MethodSignature) ((MethodSignature) methodModel.TargetDescriptor.Signature).ImportWith(importer); - - if (targetMethod.Name == ".ctor") - { - targetMethod.IsSpecialName = targetMethod.IsRuntimeSpecialName = true; - } - - if (methodModel.Definition.CilMethodBody != null) - { - var body = targetMethod.CilMethodBody = new CilMethodBody(); - body.Instructions.Add(CilOpCodes.Ldnull); - body.Instructions.Add(CilOpCodes.Throw); - } - - targetType.Methods.Add(targetMethod); - clonedMethods.Add(methodModel.Definition, targetMethod); - } - - foreach (var shimFieldModel in shimTypeModel.Fields) - { - FieldDefinition targetField; - switch (shimFieldModel) - { - case ShimFieldModel.FromField fromField: - targetField = memberCloner.CloneField(fromField.Definition); - targetField.Name = fromField.TargetDescriptor.Name; - targetField.Signature = ((FieldSignature) fromField.TargetDescriptor.Signature).ImportWith(importer); - break; - - case ShimFieldModel.FromProperty fromProperty: - var shimProperty = fromProperty.Definition; - targetField = new FieldDefinition(fromProperty.TargetDescriptor.Name, default, ((FieldSignature) fromProperty.TargetDescriptor.Signature).ImportWith(importer)) - { - IsPublic = true, - IsInitOnly = shimProperty.SetMethod == null, - }; - break; - - default: - throw new ArgumentOutOfRangeException(nameof(shimFieldModel)); - } - - targetType.Fields.Add(targetField); - } - - foreach (var shimPropertyModel in shimTypeModel.Properties) - { - var targetProperty = memberCloner.CloneProperty(shimPropertyModel.Definition, clonedMethods); - - targetType.Properties.Add(targetProperty); - } - - foreach (var shimEventModel in shimTypeModel.Events) - { - var targetEvent = memberCloner.CloneEvent(shimEventModel.Definition, clonedMethods); - - targetType.Events.Add(targetEvent); - } - } - - Strip(referenceModule); - } - - private static void ConvertExtensionMethodToInstance(MethodDefinition targetMethod) - { - if (!targetMethod.IsStatic) - { - return; - } - - ArgumentNullException.ThrowIfNull(targetMethod.Signature); - - if (targetMethod.GetExtensionAttribute() is { } extensionAttribute) - { - targetMethod.CustomAttributes.Remove(extensionAttribute); - } - - targetMethod.IsStatic = false; - targetMethod.Signature.ParameterTypes.RemoveAt(0); - targetMethod.Signature.HasThis = true; - - if (targetMethod.Parameters.ThisParameter?.Definition is { } thisParameterDefinition) - { - targetMethod.ParameterDefinitions.Remove(thisParameterDefinition); - } - - foreach (var parameterDefinition in targetMethod.ParameterDefinitions) - { - parameterDefinition.Sequence--; - } - - targetMethod.Parameters.PullUpdatesFromMethodSignature(); - } - - private static void Strip(ModuleDefinition module) - { - foreach (var method in module.GetAllTypes().SelectMany(t => t.Methods)) - { - if (method.CilMethodBody != null) - { - var body = method.CilMethodBody = new CilMethodBody(); - body.Instructions.Add(CilOpCodes.Ldnull); - body.Instructions.Add(CilOpCodes.Throw); - } - } - } - - private sealed class RedirectReferenceImporter(ModuleDefinition module) : ReferenceImporter(module) - { - public Dictionary Assemblies { get; } = new(SignatureComparer.VersionAgnostic); - public Dictionary Types { get; } = new(SignatureComparer.VersionAgnostic); - - protected override AssemblyReference ImportAssembly(AssemblyDescriptor assembly) - { - if (Assemblies.TryGetValue(assembly, out var redirected)) - { - assembly = redirected; - } - - return base.ImportAssembly(assembly); - } - - protected override ITypeDefOrRef ImportType(TypeReference type) - { - if (Types.TryGetValue(type, out var redirected)) - { - return base.ImportType(redirected); - } - - return base.ImportType(type); - } - - protected override ITypeDefOrRef ImportType(TypeDefinition type) - { - if (Types.TryGetValue(type, out var redirected)) - { - return base.ImportType(redirected); - } - - return base.ImportType(type); - } - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker/Unbreaker.cs b/src/build/CompatUnbreaker/CompatUnbreaker/Unbreaker.cs deleted file mode 100644 index a4ecdb7..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker/Unbreaker.cs +++ /dev/null @@ -1,29 +0,0 @@ -using AsmResolver.DotNet; -using CompatUnbreaker.Models; -using CompatUnbreaker.Processors; -using CompatUnbreaker.Processors.Abstractions; - -namespace CompatUnbreaker; - -public static class Unbreaker -{ - public static void ProcessConsumer(AssemblyDefinition shimAssembly, AssemblyDefinition consumerAssembly) - { - var context = new ProcessorContext - { - ShimModel = ShimModel.From(shimAssembly), - }; - - new UnbreakerConsumerProcessor().Process(context, consumerAssembly); - } - - public static void ProcessReference(AssemblyDefinition shimAssembly, AssemblyDefinition referenceAssembly) - { - var context = new ProcessorContext - { - ShimModel = ShimModel.From(shimAssembly), - }; - - new UnbreakerReferenceProcessor().Process(context, referenceAssembly); - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/CorLibTypeFactoryExtensions.cs b/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/CorLibTypeFactoryExtensions.cs deleted file mode 100644 index af925f8..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/CorLibTypeFactoryExtensions.cs +++ /dev/null @@ -1,13 +0,0 @@ -using AsmResolver.DotNet; -using AsmResolver.DotNet.Signatures; - -namespace CompatUnbreaker.Utilities.AsmResolver; - -internal static class CorLibTypeFactoryExtensions -{ - public static TypeReference Type(this CorLibTypeFactory corLibTypeFactory) - { - var scope = corLibTypeFactory.CorLibScope; - return new TypeReference(scope.ContextModule, scope, "System"u8, "Type"u8); - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.CustomAttributes.cs b/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.CustomAttributes.cs deleted file mode 100644 index 8ecc65b..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.CustomAttributes.cs +++ /dev/null @@ -1,68 +0,0 @@ -using AsmResolver.DotNet; -using AsmResolver.DotNet.Signatures; - -namespace CompatUnbreaker.Utilities.AsmResolver; - -internal readonly partial struct MemberClonerLite -{ - private void CloneCustomAttributes(IHasCustomAttribute sourceProvider, IHasCustomAttribute clonedProvider) - { - foreach (var attribute in sourceProvider.CustomAttributes) - clonedProvider.CustomAttributes.Add(CloneCustomAttribute(attribute)); - } - - private CustomAttribute CloneCustomAttribute(CustomAttribute attribute) - { - var clonedSignature = new CustomAttributeSignature(); - - if (attribute.Signature is not null) - { - // Fixed args. - foreach (var argument in attribute.Signature.FixedArguments) - clonedSignature.FixedArguments.Add(CloneCustomAttributeArgument(argument)); - - // Named args. - foreach (var namedArgument in attribute.Signature.NamedArguments) - { - var clonedArgument = new CustomAttributeNamedArgument( - namedArgument.MemberType, - namedArgument.MemberName, - namedArgument.ArgumentType, - CloneCustomAttributeArgument(namedArgument.Argument)); - - clonedSignature.NamedArguments.Add(clonedArgument); - } - } - - var constructor = attribute.Constructor; - if (constructor is null) - { - throw new ArgumentException( - $"Custom attribute of {attribute.Parent} does not have a constructor defined."); - } - - return new CustomAttribute((ICustomAttributeType) constructor.ImportWith(_importer), clonedSignature); - } - - private CustomAttributeArgument CloneCustomAttributeArgument(CustomAttributeArgument argument) - { - var clonedArgument = new CustomAttributeArgument(argument.ArgumentType.ImportWith(_importer)) - { - IsNullArray = argument.IsNullArray, - }; - - // Copy all elements. - for (var i = 0; i < argument.Elements.Count; i++) - clonedArgument.Elements.Add(CloneElement(argument.Elements[i])); - - return clonedArgument; - } - - private object? CloneElement(object? element) - { - if (element is TypeSignature type) - return type.ImportWith(_importer); - - return element; - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.Fields.cs b/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.Fields.cs deleted file mode 100644 index ffb743f..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.Fields.cs +++ /dev/null @@ -1,22 +0,0 @@ -using AsmResolver.DotNet; -using AsmResolver.DotNet.Cloning; - -namespace CompatUnbreaker.Utilities.AsmResolver; - -internal readonly partial struct MemberClonerLite -{ - private static readonly FieldRvaCloner s_fieldRvaCloner = new(); - - public FieldDefinition CloneField(FieldDefinition field) - { - var clonedField = new FieldDefinition(field.Name, field.Attributes, field.Signature?.ImportWith(_importer)); - - CloneCustomAttributes(field, clonedField); - clonedField.ImplementationMap = CloneImplementationMap(field.ImplementationMap); - clonedField.Constant = CloneConstant(field.Constant); - clonedField.FieldRva = s_fieldRvaCloner.CloneFieldRvaData(field); - clonedField.FieldOffset = field.FieldOffset; - - return clonedField; - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.GenericParameters.cs b/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.GenericParameters.cs deleted file mode 100644 index c4ae991..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.GenericParameters.cs +++ /dev/null @@ -1,31 +0,0 @@ -using AsmResolver.DotNet; - -namespace CompatUnbreaker.Utilities.AsmResolver; - -internal readonly partial struct MemberClonerLite -{ - private void CloneGenericParameters(IHasGenericParameters sourceProvider, IHasGenericParameters clonedProvider) - { - foreach (var parameter in sourceProvider.GenericParameters) - clonedProvider.GenericParameters.Add(CloneGenericParameter(parameter)); - } - - private GenericParameter CloneGenericParameter(GenericParameter parameter) - { - var clonedParameter = new GenericParameter(parameter.Name, parameter.Attributes); - - foreach (var constraint in parameter.Constraints) - clonedParameter.Constraints.Add(CloneGenericParameterConstraint(constraint)); - - CloneCustomAttributes(parameter, clonedParameter); - return clonedParameter; - } - - private GenericParameterConstraint CloneGenericParameterConstraint(GenericParameterConstraint constraint) - { - var clonedConstraint = new GenericParameterConstraint(constraint.Constraint?.ImportWith(_importer)); - - CloneCustomAttributes(constraint, clonedConstraint); - return clonedConstraint; - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.Methods.cs b/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.Methods.cs deleted file mode 100644 index 6a5bb3e..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.Methods.cs +++ /dev/null @@ -1,41 +0,0 @@ -using AsmResolver.DotNet; -using AsmResolver.DotNet.Signatures; - -namespace CompatUnbreaker.Utilities.AsmResolver; - -internal readonly partial struct MemberClonerLite -{ - public MethodDefinition CloneMethod(MethodDefinition method) - { - if (method.Name is null) - throw new ArgumentException($"Method {method} has no name."); - if (method.Signature is null) - throw new ArgumentException($"Method {method} has no signature."); - - var clonedMethod = new MethodDefinition(method.Name, method.Attributes, (MethodSignature?) method.Signature?.ImportWith(_importer)) - { - ImplAttributes = method.ImplAttributes, - }; - - clonedMethod.Parameters.PullUpdatesFromMethodSignature(); - - foreach (var parameterDef in method.ParameterDefinitions) - clonedMethod.ParameterDefinitions.Add(CloneParameterDefinition(parameterDef)); - - CloneCustomAttributes(method, clonedMethod); - CloneGenericParameters(method, clonedMethod); - CloneSecurityDeclarations(method, clonedMethod); - - clonedMethod.ImplementationMap = CloneImplementationMap(method.ImplementationMap); - - return clonedMethod; - } - - private ParameterDefinition CloneParameterDefinition(ParameterDefinition parameterDef) - { - var clonedParameterDef = new ParameterDefinition(parameterDef.Sequence, parameterDef.Name, parameterDef.Attributes); - CloneCustomAttributes(parameterDef, clonedParameterDef); - clonedParameterDef.Constant = CloneConstant(parameterDef.Constant); - return clonedParameterDef; - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.SecurityDeclarations.cs b/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.SecurityDeclarations.cs deleted file mode 100644 index e08deb5..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.SecurityDeclarations.cs +++ /dev/null @@ -1,47 +0,0 @@ -using AsmResolver.DotNet; -using AsmResolver.DotNet.Signatures; - -namespace CompatUnbreaker.Utilities.AsmResolver; - -internal readonly partial struct MemberClonerLite -{ - private void CloneSecurityDeclarations(IHasSecurityDeclaration sourceProvider, IHasSecurityDeclaration clonedProvider) - { - foreach (var declaration in sourceProvider.SecurityDeclarations) - clonedProvider.SecurityDeclarations.Add(CloneSecurityDeclaration(declaration)); - } - - private SecurityDeclaration CloneSecurityDeclaration(SecurityDeclaration declaration) - { - return new(declaration.Action, ClonePermissionSet(declaration.PermissionSet)); - } - - private PermissionSetSignature? ClonePermissionSet(PermissionSetSignature? permissionSet) - { - if (permissionSet is null) - return null; - - var result = new PermissionSetSignature(); - foreach (var attribute in permissionSet.Attributes) - result.Attributes.Add(CloneSecurityAttribute(attribute)); - return result; - } - - private SecurityAttribute CloneSecurityAttribute(SecurityAttribute attribute) - { - var result = new SecurityAttribute(attribute.AttributeType.ImportWith(_importer)); - - foreach (var argument in attribute.NamedArguments) - { - var newArgument = new CustomAttributeNamedArgument( - argument.MemberType, - argument.MemberName, - argument.ArgumentType.ImportWith(_importer), - CloneCustomAttributeArgument(argument.Argument)); - - result.NamedArguments.Add(newArgument); - } - - return result; - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.Semantics.cs b/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.Semantics.cs deleted file mode 100644 index ac685c8..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.Semantics.cs +++ /dev/null @@ -1,46 +0,0 @@ -using AsmResolver.DotNet; - -namespace CompatUnbreaker.Utilities.AsmResolver; - -internal readonly partial struct MemberClonerLite -{ - public PropertyDefinition CloneProperty(PropertyDefinition property, Dictionary? clonedMethods) - { - var clonedProperty = new PropertyDefinition( - property.Name, - property.Attributes, - property.Signature?.ImportWith(_importer) - ); - - if (clonedMethods != null) CloneSemantics(property, clonedProperty, clonedMethods); - CloneCustomAttributes(property, clonedProperty); - property.Constant = CloneConstant(property.Constant); - - return clonedProperty; - } - - public EventDefinition CloneEvent(EventDefinition @event, Dictionary? clonedMethods) - { - var clonedEvent = new EventDefinition( - @event.Name, - @event.Attributes, - @event.EventType?.ImportWith(_importer) - ); - - if (clonedMethods != null) CloneSemantics(@event, clonedEvent, clonedMethods); - CloneCustomAttributes(@event, clonedEvent); - - return clonedEvent; - } - - private static void CloneSemantics(IHasSemantics semanticsProvider, IHasSemantics clonedProvider, Dictionary clonedMethods) - { - foreach (var semantics in semanticsProvider.Semantics) - { - if (clonedMethods.TryGetValue(semantics.Method!, out var semanticMethod)) - { - clonedProvider.Semantics.Add(new MethodSemantics(semanticMethod, semantics.Attributes)); - } - } - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.Types.cs b/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.Types.cs deleted file mode 100644 index 10bfcda..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.Types.cs +++ /dev/null @@ -1,42 +0,0 @@ -using AsmResolver.DotNet; - -namespace CompatUnbreaker.Utilities.AsmResolver; - -internal partial struct MemberClonerLite -{ - public TypeDefinition CloneType(TypeDefinition type) - { - var clonedType = new TypeDefinition(type.Namespace, type.Name, type.Attributes, type.BaseType?.ImportWith(_importer)); - - // Copy interface implementations. - foreach (var implementation in type.Interfaces) - clonedType.Interfaces.Add(CloneInterfaceImplementation(implementation)); - - // Copy method implementations. - foreach (var implementation in type.MethodImplementations) - { - clonedType.MethodImplementations.Add(new MethodImplementation( - implementation.Declaration?.ImportWith(_importer), - implementation.Body?.ImportWith(_importer) - )); - } - - // Clone class layout. - if (type.ClassLayout is { } layout) - clonedType.ClassLayout = new ClassLayout(layout.PackingSize, layout.ClassSize); - - // Clone remaining metadata. - CloneCustomAttributes(type, clonedType); - CloneGenericParameters(type, clonedType); - CloneSecurityDeclarations(type, clonedType); - - return clonedType; - } - - private InterfaceImplementation CloneInterfaceImplementation(InterfaceImplementation implementation) - { - var clonedImplementation = new InterfaceImplementation(implementation.Interface?.ImportWith(_importer)); - CloneCustomAttributes(implementation, clonedImplementation); - return clonedImplementation; - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.cs b/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.cs deleted file mode 100644 index 4297bb9..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/MemberClonerLite.cs +++ /dev/null @@ -1,44 +0,0 @@ -using AsmResolver.DotNet; -using AsmResolver.DotNet.Cloning; -using AsmResolver.DotNet.Signatures; - -namespace CompatUnbreaker.Utilities.AsmResolver; - -/// -/// Methods extracted from to allow for more flexibility. -/// -internal readonly partial struct MemberClonerLite -{ - private readonly ReferenceImporter _importer; - - public MemberClonerLite(ReferenceImporter importer) - { - _importer = importer; - } - - public MemberClonerLite(ModuleDefinition targetModule) : this(targetModule.DefaultImporter) - { - } - - private ImplementationMap? CloneImplementationMap(ImplementationMap? map) - { - if (map is null) - return null; - if (map.Scope is null) - throw new ArgumentException($"Scope of implementation map {map} is null."); - - return new ImplementationMap(map.Scope.ImportWith(_importer), map.Name, map.Attributes); - } - - private static Constant? CloneConstant(Constant? constant) - { - return constant is not null - ? new Constant( - constant.Type, - constant.Value is null - ? null - : new DataBlobSignature(constant.Value.Data) - ) - : null; - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/MemberDefinitionExtensions.cs b/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/MemberDefinitionExtensions.cs deleted file mode 100644 index da79a73..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/MemberDefinitionExtensions.cs +++ /dev/null @@ -1,19 +0,0 @@ -using AsmResolver.DotNet; - -namespace CompatUnbreaker.Utilities.AsmResolver; - -internal static class MemberDefinitionExtensions -{ - public static bool IsStatic(this IMemberDefinition member) - { - return member switch - { - TypeDefinition type => type is { IsSealed: true, IsAbstract: true }, - MethodDefinition method => method.IsStatic, - FieldDefinition field => field.IsStatic, - PropertyDefinition property => property.GetMethod?.IsStatic != false && property.SetMethod?.IsStatic != false, - EventDefinition @event => @event.AddMethod?.IsStatic != false && @event.RemoveMethod?.IsStatic != false, - _ => throw new ArgumentOutOfRangeException(nameof(member)), - }; - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/MethodDefinitionExtensions.cs b/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/MethodDefinitionExtensions.cs deleted file mode 100644 index 6067645..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/MethodDefinitionExtensions.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Runtime.CompilerServices; -using AsmResolver.DotNet; - -namespace CompatUnbreaker.Utilities.AsmResolver; - -internal static class MethodDefinitionExtensions -{ - public static bool IsExtension(this MethodDefinition method) - { - return method.GetExtensionAttribute() != null; - } - - public static CustomAttribute? GetExtensionAttribute(this MethodDefinition method) - { - return method.FindCustomAttributes("System.Runtime.CompilerServices", nameof(ExtensionAttribute)).FirstOrDefault(); - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/ReferenceVisitor.cs b/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/ReferenceVisitor.cs deleted file mode 100644 index b9047e7..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/ReferenceVisitor.cs +++ /dev/null @@ -1,146 +0,0 @@ -using AsmResolver.DotNet; -using AsmResolver.DotNet.Signatures; -using CompatUnbreaker.Models; - -namespace CompatUnbreaker.Utilities.AsmResolver; - -/// -/// but visits everything regardless of whether it was already imported. -/// -internal abstract class ReferenceVisitor(ModuleDefinition module) : ReferenceImporter(module) -{ - /// - public override IResolutionScope ImportScope(IResolutionScope scope) - { - ArgumentNullException.ThrowIfNull(scope); - - return scope switch - { - AssemblyReference assembly => ImportAssembly(assembly), - TypeReference parentType => (IResolutionScope) ImportType(parentType), - ModuleDefinition moduleDef => ImportAssembly(moduleDef.Assembly ?? throw new ArgumentException("Module is not added to an assembly.")), - ModuleReference moduleRef => ImportModule(moduleRef), - _ => throw new ArgumentOutOfRangeException(nameof(scope)), - }; - } - - /// - public override IImplementation ImportImplementation(IImplementation? implementation) - { - ArgumentNullException.ThrowIfNull(implementation); - - return implementation switch - { - AssemblyReference assembly => ImportAssembly(assembly), - ExportedType type => ImportType(type), - FileReference file => ImportFile(file), - _ => throw new ArgumentOutOfRangeException(nameof(implementation)), - }; - } - - /// - protected override ITypeDefOrRef ImportType(TypeDefinition type) - { - ArgumentNullException.ThrowIfNull(type); - if (((ITypeDescriptor) type).Scope is not { } scope) - throw new ArgumentException("Cannot import a type that has not been added to a module."); - - return new TypeReference( - TargetModule, - ImportScope(scope), - type.Namespace, - type.Name); - } - - /// - protected override ITypeDefOrRef ImportType(TypeReference type) - { - ArgumentNullException.ThrowIfNull(type); - - return new TypeReference( - TargetModule, - type.Scope is not null - ? ImportScope(type.Scope) - : null, - type.Namespace, - type.Name); - } - - /// - protected override ITypeDefOrRef ImportType(TypeSpecification type) - { - ArgumentNullException.ThrowIfNull(type); - if (type.Signature is null) - throw new ArgumentNullException(nameof(type)); - - return new TypeSpecification(ImportTypeSignature(type.Signature)); - } - - /// - public override ExportedType ImportType(ExportedType type) - { - ArgumentNullException.ThrowIfNull(type); - - var result = TargetModule.ExportedTypes.FirstOrDefault(a => SignatureComparer.Default.Equals(a, type)); - - if (result is null) - { - result = new ExportedType(ImportImplementation(type.Implementation), type.Namespace, type.Name); - TargetModule.ExportedTypes.Add(result); - } - - return result; - } - - /// - public override TypeSignature ImportTypeSignature(TypeSignature type) - { - ArgumentNullException.ThrowIfNull(type); - - return type.AcceptVisitor(this); - } - - /// - public override IMethodDefOrRef ImportMethod(IMethodDefOrRef method) - { - ArgumentNullException.ThrowIfNull(method); - if (method.DeclaringType is null) - throw new ArgumentException("Cannot import a method that is not added to a type."); - if (method.Signature is null) - throw new ArgumentException("Cannot import a method that does not have a signature."); - - return new MemberReference( - ImportType(method.DeclaringType), - method.Name, - ImportMethodSignature(method.Signature)); - } - - /// - public override MethodSpecification ImportMethod(MethodSpecification method) - { - if (method.Method is null || method.Signature is null) - throw new ArgumentNullException(nameof(method)); - if (method.DeclaringType is null) - throw new ArgumentException("Cannot import a method that is not added to a type."); - - var memberRef = ImportMethod(method.Method); - var signature = ImportGenericInstanceMethodSignature(method.Signature); - - return new MethodSpecification(memberRef, signature); - } - - /// - public override IFieldDescriptor ImportField(IFieldDescriptor field) - { - ArgumentNullException.ThrowIfNull(field); - if (field.DeclaringType is null) - throw new ArgumentException("Cannot import a field that is not added to a type."); - if (field.Signature is null) - throw new ArgumentException("Cannot import a field that does not have a signature."); - - return new MemberReference( - ImportType((ITypeDefOrRef) field.DeclaringType), - field.Name, - ImportFieldSignature(field.Signature)); - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/SimpleAssemblyResolver.cs b/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/SimpleAssemblyResolver.cs deleted file mode 100644 index 43d1ef0..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/SimpleAssemblyResolver.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System.Collections.Concurrent; -using AsmResolver.DotNet; -using AsmResolver.DotNet.Serialized; -using AsmResolver.DotNet.Signatures; - -namespace CompatUnbreaker.Utilities.AsmResolver; - -public sealed class SimpleAssemblyResolver : IAssemblyResolver -{ - private readonly ConcurrentDictionary _assemblies; - - public SimpleAssemblyResolver( - IEnumerable? assemblies = null, - SignatureComparisonFlags comparisonFlags = SignatureComparisonFlags.VersionAgnostic - ) - { - var signatureComparer = new SignatureComparer(comparisonFlags); - - _assemblies = new ConcurrentDictionary(signatureComparer); - - if (assemblies != null) - { - foreach (var assembly in assemblies) - { - Load(assembly); - } - } - } - - public AssemblyDefinition? Resolve(AssemblyDescriptor assembly) - { - if (_assemblies.TryGetValue(assembly, out var result)) - { - return result; - } - - return null; - } - - public void AddToCache(AssemblyDescriptor descriptor, AssemblyDefinition definition) - { - if (!_assemblies.TryAdd(descriptor, definition)) - { - throw new ArgumentException($"An item with the same key has already been added. Key: {descriptor}"); - } - } - - public bool RemoveFromCache(AssemblyDescriptor descriptor) - { - return _assemblies.TryRemove(descriptor, out _); - } - - public bool HasCached(AssemblyDescriptor descriptor) - { - return _assemblies.ContainsKey(descriptor); - } - - public void ClearCache() - { - _assemblies.Clear(); - } - - public void Add(AssemblyDefinition definition) - { - AddToCache(definition, definition); - } - - public void Load(AssemblyDefinition definition) - { - foreach (var module in definition.Modules) - { - module.MetadataResolver = new DefaultMetadataResolver(this); - } - - Add(definition); - } - - public AssemblyDefinition Load(string filePath) - { - var assemblyDefinition = AssemblyDefinition.FromFile(filePath, new ModuleReaderParameters()); - Load(assemblyDefinition); - return assemblyDefinition; - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/TypeDefinitionExtensions.cs b/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/TypeDefinitionExtensions.cs deleted file mode 100644 index d864d4b..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/TypeDefinitionExtensions.cs +++ /dev/null @@ -1,86 +0,0 @@ -using AsmResolver.DotNet; -using AsmResolver.DotNet.Signatures; - -namespace CompatUnbreaker.Utilities.AsmResolver; - -internal static class TypeDefinitionExtensions -{ - public static MethodDefinition? TryGetExtensionMarkerMethod(this TypeDefinition type) - { - const string ExtensionMarkerMethodName = "$"; - - foreach (var method in type.Methods) - { - if (method.IsSpecialName && method.Name == ExtensionMarkerMethodName) - { - return method; - } - } - - return null; - } - - public static MethodDefinition? FindCorrespondingExtensionImplementationMethod(this TypeDefinition @this, MethodDefinition method, TypeSignature extensionParameter) - { - foreach (var candidate in @this.DeclaringType!.Methods) - { - if (!candidate.IsStatic || candidate.Name != method.Name) - { - continue; - } - - var signature = method.Signature!; - var candidateSignature = candidate.Signature!; - - if (candidateSignature.GenericParameterCount != @this.GenericParameters.Count + signature.GenericParameterCount) - { - continue; - } - - var additionalParameterCount = method.IsStatic ? 0 : 1; - if (candidateSignature.ParameterTypes.Count != additionalParameterCount + signature.ParameterTypes.Count) - { - continue; - } - - var typeMap = new Dictionary(SignatureComparer.Default); - { - var index = 0; - - for (var i = 0; i < @this.GenericParameters.Count; i++) - { - typeMap[new GenericParameterSignature(GenericParameterType.Type, i)] = new GenericParameterSignature(GenericParameterType.Method, index++); - } - - for (var i = 0; i < signature.GenericParameterCount; i++) - { - typeMap[new GenericParameterSignature(GenericParameterType.Method, i)] = new GenericParameterSignature(GenericParameterType.Method, index++); - } - } - - var typeMapVisitor = new TypeMapVisitor(typeMap); - - if (!SignatureComparer.Default.Equals(candidateSignature.ReturnType, signature.ReturnType.AcceptVisitor(typeMapVisitor))) - { - continue; - } - - if (!method.IsStatic && !SignatureComparer.Default.Equals(candidateSignature.ParameterTypes[0], extensionParameter.AcceptVisitor(typeMapVisitor))) - { - continue; - } - - if (!SignatureComparer.Default.Equals( - candidateSignature.ParameterTypes.Skip(additionalParameterCount), - signature.ParameterTypes.Select(s => s.AcceptVisitor(typeMapVisitor)) - )) - { - continue; - } - - return candidate; - } - - return null; - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/TypeMapVisitor.cs b/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/TypeMapVisitor.cs deleted file mode 100644 index 3375f25..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker/Utilities/AsmResolver/TypeMapVisitor.cs +++ /dev/null @@ -1,127 +0,0 @@ -using AsmResolver.DotNet; -using AsmResolver.DotNet.Signatures; - -namespace CompatUnbreaker.Utilities.AsmResolver; - -internal sealed class TypeMapVisitor( - Dictionary typeMap -) : ITypeSignatureVisitor -{ - TypeSignature ITypeSignatureVisitor.VisitArrayType(ArrayTypeSignature signature) - { - if (typeMap.TryGetValue(signature, out var mapped)) return mapped; - - var result = new ArrayTypeSignature(signature.BaseType.AcceptVisitor(this)); - foreach (var dimension in signature.Dimensions) - result.Dimensions.Add(new ArrayDimension(dimension.Size, dimension.LowerBound)); - return result; - } - - TypeSignature ITypeSignatureVisitor.VisitBoxedType(BoxedTypeSignature signature) - { - if (typeMap.TryGetValue(signature, out var mapped)) return mapped; - - return new BoxedTypeSignature(signature.BaseType.AcceptVisitor(this)); - } - - TypeSignature ITypeSignatureVisitor.VisitByReferenceType(ByReferenceTypeSignature signature) - { - if (typeMap.TryGetValue(signature, out var mapped)) return mapped; - - return new ByReferenceTypeSignature(signature.BaseType.AcceptVisitor(this)); - } - - TypeSignature ITypeSignatureVisitor.VisitCorLibType(CorLibTypeSignature signature) - { - return typeMap.GetValueOrDefault(signature, signature); - } - - TypeSignature ITypeSignatureVisitor.VisitCustomModifierType(CustomModifierTypeSignature signature) - { - if (typeMap.TryGetValue(signature, out var mapped)) return mapped; - - return new CustomModifierTypeSignature(VisitType(signature.ModifierType), signature.IsRequired, signature.BaseType.AcceptVisitor(this)); - } - - TypeSignature ITypeSignatureVisitor.VisitGenericInstanceType(GenericInstanceTypeSignature signature) - { - if (typeMap.TryGetValue(signature, out var mapped)) return mapped; - - var result = new GenericInstanceTypeSignature(VisitType(signature.GenericType), signature.IsValueType); - foreach (var argument in signature.TypeArguments) - result.TypeArguments.Add(argument.AcceptVisitor(this)); - return result; - } - - TypeSignature ITypeSignatureVisitor.VisitGenericParameter(GenericParameterSignature signature) - { - return typeMap.GetValueOrDefault(signature, signature); - } - - TypeSignature ITypeSignatureVisitor.VisitPinnedType(PinnedTypeSignature signature) - { - if (typeMap.TryGetValue(signature, out var mapped)) return mapped; - - return new PinnedTypeSignature(signature.BaseType.AcceptVisitor(this)); - } - - TypeSignature ITypeSignatureVisitor.VisitPointerType(PointerTypeSignature signature) - { - if (typeMap.TryGetValue(signature, out var mapped)) return mapped; - - return new PointerTypeSignature(signature.BaseType.AcceptVisitor(this)); - } - - TypeSignature ITypeSignatureVisitor.VisitSentinelType(SentinelTypeSignature signature) - { - return typeMap.GetValueOrDefault(signature, signature); - } - - TypeSignature ITypeSignatureVisitor.VisitSzArrayType(SzArrayTypeSignature signature) - { - if (typeMap.TryGetValue(signature, out var mapped)) return mapped; - - return new SzArrayTypeSignature(signature.BaseType.AcceptVisitor(this)); - } - - TypeSignature ITypeSignatureVisitor.VisitTypeDefOrRef(TypeDefOrRefSignature signature) - { - if (typeMap.TryGetValue(signature, out var mapped)) return mapped; - - return new TypeDefOrRefSignature(VisitType(signature.Type), signature.IsValueType); - } - - TypeSignature ITypeSignatureVisitor.VisitFunctionPointerType(FunctionPointerTypeSignature signature) - { - if (typeMap.TryGetValue(signature, out var mapped)) return mapped; - - return new FunctionPointerTypeSignature(VisitMethodSignature(signature.Signature)); - } - - private ITypeDefOrRef VisitType(ITypeDefOrRef type) - { - if (type is TypeSpecification { Signature: { } signature }) - { - return new TypeSpecification(signature.AcceptVisitor(this)); - } - - return type; - } - - private MethodSignature VisitMethodSignature(MethodSignature signature) - { - var parameterTypes = new TypeSignature[signature.ParameterTypes.Count]; - for (var i = 0; i < parameterTypes.Length; i++) - parameterTypes[i] = signature.ParameterTypes[i].AcceptVisitor(this); - - var result = new MethodSignature(signature.Attributes, signature.ReturnType.AcceptVisitor(this), parameterTypes) - { - GenericParameterCount = signature.GenericParameterCount, - }; - - for (var i = 0; i < signature.SentinelParameterTypes.Count; i++) - result.SentinelParameterTypes.Add(signature.SentinelParameterTypes[i].AcceptVisitor(this)); - - return result; - } -} diff --git a/src/build/CompatUnbreaker/CompatUnbreaker/packages.lock.json b/src/build/CompatUnbreaker/CompatUnbreaker/packages.lock.json deleted file mode 100644 index d4ca974..0000000 --- a/src/build/CompatUnbreaker/CompatUnbreaker/packages.lock.json +++ /dev/null @@ -1,141 +0,0 @@ -{ - "version": 1, - "dependencies": { - "net6.0": { - "AsmResolver.DotNet": { - "type": "Direct", - "requested": "[6.0.0-dev, )", - "resolved": "6.0.0-dev", - "dependencies": { - "AsmResolver.PE": "6.0.0-dev" - } - }, - "Meziantou.Analyzer": { - "type": "Direct", - "requested": "[2.0.220, )", - "resolved": "2.0.220", - "contentHash": "LknH5tduuxY6jzPGUIBHrszk1fepyEWAD9Ht7Hq1rrGZ02BKx54ADXzrlXTkLNpnkVzRBHU4bO6HH7GwIeDR+w==" - }, - "Microsoft.CodeAnalysis.PublicApiAnalyzers": { - "type": "Direct", - "requested": "[4.14.0, )", - "resolved": "4.14.0", - "contentHash": "5zBDSa8D1pfn5VyDzRekcYdVx/DGAzwegblVJhzdN0kvPt97TPr12haA6ZG0wS2WgBb1W42GeZJRiRtlNaoY1w==" - }, - "PolySharp": { - "type": "Direct", - "requested": "[1.15.0, )", - "resolved": "1.15.0", - "contentHash": "FbU0El+EEjdpuIX4iDbeS7ki1uzpJPx8vbqOzEtqnl1GZeAGJfq+jCbxeJL2y0EPnUNk8dRnnqR2xnYXg9Tf+g==" - }, - "StyleCop.Analyzers": { - "type": "Direct", - "requested": "[1.2.0-beta.556, )", - "resolved": "1.2.0-beta.556", - "contentHash": "llRPgmA1fhC0I0QyFLEcjvtM2239QzKr/tcnbsjArLMJxJlu0AA5G7Fft0OI30pHF3MW63Gf4aSSsjc5m82J1Q==", - "dependencies": { - "StyleCop.Analyzers.Unstable": "1.2.0.556" - } - }, - "AsmResolver": { - "type": "Transitive", - "resolved": "6.0.0-dev" - }, - "AsmResolver.PE": { - "type": "Transitive", - "resolved": "6.0.0-dev", - "dependencies": { - "AsmResolver.PE.File": "6.0.0-dev" - } - }, - "AsmResolver.PE.File": { - "type": "Transitive", - "resolved": "6.0.0-dev", - "dependencies": { - "AsmResolver": "6.0.0-dev" - } - }, - "StyleCop.Analyzers.Unstable": { - "type": "Transitive", - "resolved": "1.2.0.556", - "contentHash": "zvn9Mqs/ox/83cpYPignI8hJEM2A93s2HkHs8HYMOAQW0PkampyoErAiIyKxgTLqbbad29HX/shv/6LGSjPJNQ==" - }, - "compatunbreaker.attributes": { - "type": "Project", - "dependencies": { - "Meziantou.Analyzer": "[2.0.220, )", - "PolySharp": "[1.15.0, )", - "StyleCop.Analyzers": "[1.2.0-beta.556, )" - } - } - }, - "net9.0": { - "AsmResolver.DotNet": { - "type": "Direct", - "requested": "[6.0.0-dev, )", - "resolved": "6.0.0-dev", - "dependencies": { - "AsmResolver.PE": "6.0.0-dev" - } - }, - "Meziantou.Analyzer": { - "type": "Direct", - "requested": "[2.0.220, )", - "resolved": "2.0.220", - "contentHash": "LknH5tduuxY6jzPGUIBHrszk1fepyEWAD9Ht7Hq1rrGZ02BKx54ADXzrlXTkLNpnkVzRBHU4bO6HH7GwIeDR+w==" - }, - "Microsoft.CodeAnalysis.PublicApiAnalyzers": { - "type": "Direct", - "requested": "[4.14.0, )", - "resolved": "4.14.0", - "contentHash": "5zBDSa8D1pfn5VyDzRekcYdVx/DGAzwegblVJhzdN0kvPt97TPr12haA6ZG0wS2WgBb1W42GeZJRiRtlNaoY1w==" - }, - "PolySharp": { - "type": "Direct", - "requested": "[1.15.0, )", - "resolved": "1.15.0", - "contentHash": "FbU0El+EEjdpuIX4iDbeS7ki1uzpJPx8vbqOzEtqnl1GZeAGJfq+jCbxeJL2y0EPnUNk8dRnnqR2xnYXg9Tf+g==" - }, - "StyleCop.Analyzers": { - "type": "Direct", - "requested": "[1.2.0-beta.556, )", - "resolved": "1.2.0-beta.556", - "contentHash": "llRPgmA1fhC0I0QyFLEcjvtM2239QzKr/tcnbsjArLMJxJlu0AA5G7Fft0OI30pHF3MW63Gf4aSSsjc5m82J1Q==", - "dependencies": { - "StyleCop.Analyzers.Unstable": "1.2.0.556" - } - }, - "AsmResolver": { - "type": "Transitive", - "resolved": "6.0.0-dev" - }, - "AsmResolver.PE": { - "type": "Transitive", - "resolved": "6.0.0-dev", - "dependencies": { - "AsmResolver.PE.File": "6.0.0-dev" - } - }, - "AsmResolver.PE.File": { - "type": "Transitive", - "resolved": "6.0.0-dev", - "dependencies": { - "AsmResolver": "6.0.0-dev" - } - }, - "StyleCop.Analyzers.Unstable": { - "type": "Transitive", - "resolved": "1.2.0.556", - "contentHash": "zvn9Mqs/ox/83cpYPignI8hJEM2A93s2HkHs8HYMOAQW0PkampyoErAiIyKxgTLqbbad29HX/shv/6LGSjPJNQ==" - }, - "compatunbreaker.attributes": { - "type": "Project", - "dependencies": { - "Meziantou.Analyzer": "[2.0.220, )", - "PolySharp": "[1.15.0, )", - "StyleCop.Analyzers": "[1.2.0-beta.556, )" - } - } - } - } -} \ No newline at end of file diff --git a/src/build/CompatUnbreaker/Directory.Build.props b/src/build/CompatUnbreaker/Directory.Build.props deleted file mode 100644 index c17d709..0000000 --- a/src/build/CompatUnbreaker/Directory.Build.props +++ /dev/null @@ -1,16 +0,0 @@ - - - - - true - $(MSBuildThisFileDirectory)artifacts - - - - js6pak - 2025 js6pak - MIT - https://github.com/js6pak/CompatUnbreaker - git - - diff --git a/src/build/CompatUnbreaker/PlaygroundLibrary/Directory.Build.props b/src/build/CompatUnbreaker/PlaygroundLibrary/Directory.Build.props deleted file mode 100644 index 00ce6a1..0000000 --- a/src/build/CompatUnbreaker/PlaygroundLibrary/Directory.Build.props +++ /dev/null @@ -1,10 +0,0 @@ - - - - - PlaygroundLibrary - PlaygroundLibrary - net9.0 - true - - diff --git a/src/build/CompatUnbreaker/PlaygroundLibrary/PlaygroundLibrary.V1.csproj b/src/build/CompatUnbreaker/PlaygroundLibrary/PlaygroundLibrary.V1.csproj deleted file mode 100644 index 67562d7..0000000 --- a/src/build/CompatUnbreaker/PlaygroundLibrary/PlaygroundLibrary.V1.csproj +++ /dev/null @@ -1,6 +0,0 @@ - - - 1.0.0 - V1 - - diff --git a/src/build/CompatUnbreaker/PlaygroundLibrary/PlaygroundLibrary.V2.csproj b/src/build/CompatUnbreaker/PlaygroundLibrary/PlaygroundLibrary.V2.csproj deleted file mode 100644 index f78ad0a..0000000 --- a/src/build/CompatUnbreaker/PlaygroundLibrary/PlaygroundLibrary.V2.csproj +++ /dev/null @@ -1,6 +0,0 @@ - - - 2.0.0 - V2 - - diff --git a/src/build/CompatUnbreaker/PlaygroundLibrary/TestClass.cs b/src/build/CompatUnbreaker/PlaygroundLibrary/TestClass.cs deleted file mode 100644 index c6914a6..0000000 --- a/src/build/CompatUnbreaker/PlaygroundLibrary/TestClass.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -#if V1 -namespace PlaygroundLibrary; - -[Display] -public class TestClass -{ - private readonly int readonlyField; - - [Required] - public int Property { get => throw null!; set => throw null!; } - - public int GetOnlyProperty { get => throw null!; } - public int SetOnlyProperty { set => throw null!; } - public int InitOnlyProperty { get => throw null!; init => throw null!; } - - [Obsolete] - public event Action Event - { - add => throw null!; - remove => throw null!; - } -} - -public struct Struct -{ - public int a; - public readonly int b; -} - -public record Record(int a, int b); - -public readonly record struct StructRecord(int a, int b); - -public ref struct RefStruct -{ - public int a; - public ref int b; - public ref readonly int c; - public readonly ref readonly int d; -} -#endif diff --git a/src/build/CompatUnbreaker/PlaygroundLibrary/packages.lock.json b/src/build/CompatUnbreaker/PlaygroundLibrary/packages.lock.json deleted file mode 100644 index 8803aad..0000000 --- a/src/build/CompatUnbreaker/PlaygroundLibrary/packages.lock.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "version": 1, - "dependencies": { - "net9.0": { - "Meziantou.Analyzer": { - "type": "Direct", - "requested": "[2.0.220, )", - "resolved": "2.0.220", - "contentHash": "LknH5tduuxY6jzPGUIBHrszk1fepyEWAD9Ht7Hq1rrGZ02BKx54ADXzrlXTkLNpnkVzRBHU4bO6HH7GwIeDR+w==" - }, - "PolySharp": { - "type": "Direct", - "requested": "[1.15.0, )", - "resolved": "1.15.0", - "contentHash": "FbU0El+EEjdpuIX4iDbeS7ki1uzpJPx8vbqOzEtqnl1GZeAGJfq+jCbxeJL2y0EPnUNk8dRnnqR2xnYXg9Tf+g==" - }, - "StyleCop.Analyzers": { - "type": "Direct", - "requested": "[1.2.0-beta.556, )", - "resolved": "1.2.0-beta.556", - "contentHash": "llRPgmA1fhC0I0QyFLEcjvtM2239QzKr/tcnbsjArLMJxJlu0AA5G7Fft0OI30pHF3MW63Gf4aSSsjc5m82J1Q==", - "dependencies": { - "StyleCop.Analyzers.Unstable": "1.2.0.556" - } - }, - "StyleCop.Analyzers.Unstable": { - "type": "Transitive", - "resolved": "1.2.0.556", - "contentHash": "zvn9Mqs/ox/83cpYPignI8hJEM2A93s2HkHs8HYMOAQW0PkampyoErAiIyKxgTLqbbad29HX/shv/6LGSjPJNQ==" - } - } - } -} \ No newline at end of file diff --git a/src/build/CompatUnbreaker/global.json b/src/build/CompatUnbreaker/global.json deleted file mode 100644 index 93792dc..0000000 --- a/src/build/CompatUnbreaker/global.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "sdk": { - "version": "9.0.300", - "rollForward": "latestPatch" - } -} diff --git a/src/build/CompatUnbreaker/nuget.config b/src/build/CompatUnbreaker/nuget.config deleted file mode 100644 index 07e0d45..0000000 --- a/src/build/CompatUnbreaker/nuget.config +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - From fbae9eea10b7633ce40be99db3cef220c4646d2f Mon Sep 17 00:00:00 2001 From: DaNike Date: Wed, 5 Nov 2025 21:57:40 -0600 Subject: [PATCH 09/15] Clean up warnings in ArApiCompat --- .../AssemblyMapping/AssemblyMapper.cs | 15 ++++++++------- .../AssemblyMapping/ElementMapper.cs | 8 +++++--- .../AssemblyMapping/ElementSide.cs | 2 +- .../AssemblyMapping/MapperSettings.cs | 8 +++----- .../AssemblyMapping/MemberMapper.cs | 4 ++-- .../AssemblyMapping/TypeMapper.cs | 13 +++++++------ .../ApiCompatibility/Comparing/ApiComparer.cs | 10 ++++++---- .../Comparing/CompatDifference.cs | 4 ++-- .../ApiCompatibility/Comparing/DifferenceType.cs | 2 +- .../ApiCompatibility/Comparing/Rules/BaseRule.cs | 4 ++-- .../Comparing/Rules/CannotAddAbstractMember.cs | 7 +++---- .../Rules/CannotAddMemberToInterface.cs | 4 ++-- .../Rules/CannotAddOrRemoveVirtualKeyword.cs | 7 +++---- .../Rules/CannotChangeGenericConstraints.cs | 16 +++++++++------- .../Comparing/Rules/CannotChangeVisibility.cs | 10 ++++------ .../Rules/CannotRemoveBaseTypeOrInterface.cs | 10 +++++----- .../Comparing/Rules/CannotSealType.cs | 7 +++---- .../Comparing/Rules/EnumsMustMatch.cs | 6 +++--- .../Comparing/Rules/MembersMustExist.cs | 5 +++-- src/build/ArApiCompat/ArApiCompat.csproj | 1 + .../Utilities/AsmResolver/Accessibility.cs | 2 +- .../AsmResolver/AccessibilityExtensions.cs | 8 ++++---- .../AsmResolver/DefinitionModifiersExtensions.cs | 1 + .../AsmResolver/ExtendedSignatureComparer.cs | 2 +- .../AsmResolver/GenericParameterExtensions.cs | 2 +- .../AsmResolver/MemberDefinitionExtensions.cs | 2 +- .../AsmResolver/MethodDefinitionExtensions.cs | 2 +- .../AsmResolver/MiscellaneousExtensions.cs | 2 +- .../TypeDefinitionExtensions.InterfaceMapping.cs | 1 + .../AsmResolver/TypeDefinitionExtensions.cs | 1 + .../AsmResolver/TypeSignatureExtensions.cs | 2 +- .../AsmResolver/VisibilityExtensions.cs | 2 +- .../ArApiCompat/Utilities/MetadataHelpers.cs | 2 +- .../ArApiCompat/Utilities/StringExtensions.cs | 2 +- 34 files changed, 90 insertions(+), 84 deletions(-) diff --git a/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/AssemblyMapper.cs b/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/AssemblyMapper.cs index 18136cc..34ab791 100644 --- a/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/AssemblyMapper.cs +++ b/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/AssemblyMapper.cs @@ -1,9 +1,9 @@ +using ArApiCompat.Utilities.AsmResolver; using AsmResolver.DotNet; -using CompatUnbreaker.Tool.Utilities.AsmResolver; -namespace CompatUnbreaker.Tool.ApiCompatibility.AssemblyMapping; +namespace ArApiCompat.ApiCompatibility.AssemblyMapping; -public sealed class AssemblyMapper(MapperSettings settings) : ElementMapper +public sealed class AssemblyMapper(MapperSettings MapperSettings) : ElementMapper(MapperSettings) { private readonly Dictionary _types = new(ExtendedSignatureComparer.VersionAgnostic); @@ -17,8 +17,9 @@ public override void Add(AssemblyDefinition value, ElementSide side) { foreach (var type in module.TopLevelTypes) { - if (type.Namespace is null || !type.Namespace.Value.StartsWith("System.Threading.Tasks")) continue; - if (settings.Filter(type)) + if (type.Namespace is null) continue; + + if (MapperSettings.Filter(type)) { AddOrCreateMapper(type, side); } @@ -33,7 +34,7 @@ public override void Add(AssemblyDefinition value, ElementSide side) return; } - if (settings.Filter(type)) + if (MapperSettings.Filter(type)) { AddOrCreateMapper(type, side); } @@ -45,7 +46,7 @@ private void AddOrCreateMapper(TypeDefinition type, ElementSide side) { if (!_types.TryGetValue(type, out var mapper)) { - mapper = new TypeMapper(settings); + mapper = new TypeMapper(MapperSettings); _types.Add(type, mapper); } diff --git a/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/ElementMapper.cs b/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/ElementMapper.cs index 9062c7f..dd20492 100644 --- a/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/ElementMapper.cs +++ b/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/ElementMapper.cs @@ -1,7 +1,9 @@ -namespace CompatUnbreaker.Tool.ApiCompatibility.AssemblyMapping; +namespace ArApiCompat.ApiCompatibility.AssemblyMapping; + +public abstract class ElementMapper(MapperSettings mapperSettings) +{ + public MapperSettings MapperSettings { get; } = mapperSettings; -public abstract class ElementMapper -{ public T? Left { get; set; } public T? Right { get; set; } diff --git a/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/ElementSide.cs b/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/ElementSide.cs index 0bb249d..6021620 100644 --- a/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/ElementSide.cs +++ b/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/ElementSide.cs @@ -1,4 +1,4 @@ -namespace CompatUnbreaker.Tool.ApiCompatibility.AssemblyMapping; +namespace ArApiCompat.ApiCompatibility.AssemblyMapping; public enum ElementSide : byte { diff --git a/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/MapperSettings.cs b/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/MapperSettings.cs index 0e3c3ee..bcd305a 100644 --- a/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/MapperSettings.cs +++ b/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/MapperSettings.cs @@ -1,8 +1,7 @@ +using ArApiCompat.Utilities.AsmResolver; using AsmResolver.DotNet; -using CompatUnbreaker.Tool.Utilities.AsmResolver; -using CompatUnbreaker.Utilities.AsmResolver; -namespace CompatUnbreaker.Tool.ApiCompatibility.AssemblyMapping; +namespace ArApiCompat.ApiCompatibility.AssemblyMapping; public sealed class MapperSettings { @@ -10,7 +9,6 @@ public sealed class MapperSettings private static bool DefaultFilter(IMemberDefinition member) { - return member.IsVisibleOutsideOfAssembly() && - (member is not TypeDefinition type || type.Namespace != "System.Runtime.CompilerServices"); + return member.IsVisibleOutsideOfAssembly(); } } diff --git a/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/MemberMapper.cs b/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/MemberMapper.cs index a9f1b70..795cbc0 100644 --- a/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/MemberMapper.cs +++ b/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/MemberMapper.cs @@ -1,8 +1,8 @@ using AsmResolver.DotNet; -namespace CompatUnbreaker.Tool.ApiCompatibility.AssemblyMapping; +namespace ArApiCompat.ApiCompatibility.AssemblyMapping; -public sealed class MemberMapper(MapperSettings settings, TypeMapper declaringType) : ElementMapper +public sealed class MemberMapper(MapperSettings settings, TypeMapper declaringType) : ElementMapper(settings) { public TypeMapper DeclaringType { get; } = declaringType; } diff --git a/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/TypeMapper.cs b/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/TypeMapper.cs index b6dd365..cc90364 100644 --- a/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/TypeMapper.cs +++ b/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/TypeMapper.cs @@ -1,9 +1,10 @@ +using ArApiCompat.Utilities.AsmResolver; using AsmResolver.DotNet; using CompatUnbreaker.Tool.Utilities.AsmResolver; -namespace CompatUnbreaker.Tool.ApiCompatibility.AssemblyMapping; +namespace ArApiCompat.ApiCompatibility.AssemblyMapping; -public sealed class TypeMapper(MapperSettings settings, TypeMapper? declaringType = null) : ElementMapper +public sealed class TypeMapper(MapperSettings MapperSettings, TypeMapper? declaringType = null) : ElementMapper(MapperSettings) { private readonly Dictionary _nestedTypes = new(ExtendedSignatureComparer.VersionAgnostic); private readonly Dictionary _members = new(ExtendedSignatureComparer.VersionAgnostic); @@ -19,7 +20,7 @@ public override void Add(TypeDefinition value, ElementSide side) foreach (var member in value.GetMembers(includeNestedTypes: false)) { - if (settings.Filter(member)) + if (MapperSettings.Filter(member)) { AddOrCreateMapper(member, side); } @@ -27,7 +28,7 @@ public override void Add(TypeDefinition value, ElementSide side) foreach (var nestedType in value.NestedTypes) { - if (settings.Filter(nestedType)) + if (MapperSettings.Filter(nestedType)) { AddOrCreateMapper(nestedType, side); } @@ -38,7 +39,7 @@ private void AddOrCreateMapper(TypeDefinition nestedType, ElementSide side) { if (!_nestedTypes.TryGetValue(nestedType, out var mapper)) { - mapper = new TypeMapper(settings, this); + mapper = new TypeMapper(MapperSettings, this); _nestedTypes.Add(nestedType, mapper); } @@ -49,7 +50,7 @@ private void AddOrCreateMapper(IMemberDefinition member, ElementSide side) { if (!_members.TryGetValue(member, out var mapper)) { - mapper = new MemberMapper(settings, this); + mapper = new MemberMapper(MapperSettings, this); _members.Add(member, mapper); } diff --git a/src/build/ArApiCompat/ApiCompatibility/Comparing/ApiComparer.cs b/src/build/ArApiCompat/ApiCompatibility/Comparing/ApiComparer.cs index b07ffb8..a8ea03f 100644 --- a/src/build/ArApiCompat/ApiCompatibility/Comparing/ApiComparer.cs +++ b/src/build/ArApiCompat/ApiCompatibility/Comparing/ApiComparer.cs @@ -1,7 +1,7 @@ -using CompatUnbreaker.Tool.ApiCompatibility.AssemblyMapping; -using CompatUnbreaker.Tool.ApiCompatibility.Comparing.Rules; +using ArApiCompat.ApiCompatibility.AssemblyMapping; +using ArApiCompat.ApiCompatibility.Comparing.Rules; -namespace CompatUnbreaker.Tool.ApiCompatibility.Comparing; +namespace ArApiCompat.ApiCompatibility.Comparing; public sealed class ApiComparer { @@ -24,7 +24,9 @@ public sealed class ApiComparer public IEnumerable CompatDifferences => _compatDifferences; public void Compare(AssemblyMapper assemblyMapper) - { + { + ArgumentNullException.ThrowIfNull(assemblyMapper); + AddDifferences(assemblyMapper); foreach (var typeMapper in assemblyMapper.Types) diff --git a/src/build/ArApiCompat/ApiCompatibility/Comparing/CompatDifference.cs b/src/build/ArApiCompat/ApiCompatibility/Comparing/CompatDifference.cs index e7302a3..cf17b97 100644 --- a/src/build/ArApiCompat/ApiCompatibility/Comparing/CompatDifference.cs +++ b/src/build/ArApiCompat/ApiCompatibility/Comparing/CompatDifference.cs @@ -1,7 +1,7 @@ +using ArApiCompat.ApiCompatibility.AssemblyMapping; using AsmResolver.DotNet; -using CompatUnbreaker.Tool.ApiCompatibility.AssemblyMapping; -namespace CompatUnbreaker.Tool.ApiCompatibility.Comparing; +namespace ArApiCompat.ApiCompatibility.Comparing; public abstract class CompatDifference { diff --git a/src/build/ArApiCompat/ApiCompatibility/Comparing/DifferenceType.cs b/src/build/ArApiCompat/ApiCompatibility/Comparing/DifferenceType.cs index 328c0db..58d3a78 100644 --- a/src/build/ArApiCompat/ApiCompatibility/Comparing/DifferenceType.cs +++ b/src/build/ArApiCompat/ApiCompatibility/Comparing/DifferenceType.cs @@ -1,4 +1,4 @@ -namespace CompatUnbreaker.Tool.ApiCompatibility.Comparing; +namespace ArApiCompat.ApiCompatibility.Comparing; public enum DifferenceType { diff --git a/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/BaseRule.cs b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/BaseRule.cs index 10f5f76..b6c183e 100644 --- a/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/BaseRule.cs +++ b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/BaseRule.cs @@ -1,6 +1,6 @@ -using CompatUnbreaker.Tool.ApiCompatibility.AssemblyMapping; +using ArApiCompat.ApiCompatibility.AssemblyMapping; -namespace CompatUnbreaker.Tool.ApiCompatibility.Comparing.Rules; +namespace ArApiCompat.ApiCompatibility.Comparing.Rules; public abstract class BaseRule { diff --git a/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotAddAbstractMember.cs b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotAddAbstractMember.cs index dffe6b2..142ebb6 100644 --- a/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotAddAbstractMember.cs +++ b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotAddAbstractMember.cs @@ -1,8 +1,7 @@ -using CompatUnbreaker.Tool.ApiCompatibility.AssemblyMapping; -using CompatUnbreaker.Tool.Utilities.AsmResolver; -using CompatUnbreaker.Utilities.AsmResolver; +using ArApiCompat.ApiCompatibility.AssemblyMapping; +using ArApiCompat.Utilities.AsmResolver; -namespace CompatUnbreaker.Tool.ApiCompatibility.Comparing.Rules; +namespace ArApiCompat.ApiCompatibility.Comparing.Rules; public sealed class CannotAddAbstractMemberDifference(MemberMapper mapper) : MemberCompatDifference(mapper) { diff --git a/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotAddMemberToInterface.cs b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotAddMemberToInterface.cs index 999f3f1..9f7bcd3 100644 --- a/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotAddMemberToInterface.cs +++ b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotAddMemberToInterface.cs @@ -1,8 +1,8 @@ +using ArApiCompat.ApiCompatibility.AssemblyMapping; using AsmResolver.DotNet; -using CompatUnbreaker.Tool.ApiCompatibility.AssemblyMapping; using CompatUnbreaker.Tool.Utilities.AsmResolver; -namespace CompatUnbreaker.Tool.ApiCompatibility.Comparing.Rules; +namespace ArApiCompat.ApiCompatibility.Comparing.Rules; public sealed class CannotAddMemberToInterfaceDifference(MemberMapper mapper) : MemberCompatDifference(mapper) { diff --git a/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotAddOrRemoveVirtualKeyword.cs b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotAddOrRemoveVirtualKeyword.cs index 673a62b..5437713 100644 --- a/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotAddOrRemoveVirtualKeyword.cs +++ b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotAddOrRemoveVirtualKeyword.cs @@ -1,9 +1,8 @@ +using ArApiCompat.ApiCompatibility.AssemblyMapping; +using ArApiCompat.Utilities.AsmResolver; using AsmResolver.DotNet; -using CompatUnbreaker.Tool.ApiCompatibility.AssemblyMapping; -using CompatUnbreaker.Tool.Utilities.AsmResolver; -using CompatUnbreaker.Utilities.AsmResolver; -namespace CompatUnbreaker.Tool.ApiCompatibility.Comparing.Rules; +namespace ArApiCompat.ApiCompatibility.Comparing.Rules; public sealed class CannotAddSealedToInterfaceMemberDifference(MemberMapper mapper) : MemberCompatDifference(mapper) { diff --git a/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotChangeGenericConstraints.cs b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotChangeGenericConstraints.cs index a5dc73e..9e5a7bd 100644 --- a/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotChangeGenericConstraints.cs +++ b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotChangeGenericConstraints.cs @@ -1,15 +1,17 @@ using System.Diagnostics; +using ArApiCompat.ApiCompatibility.AssemblyMapping; +using ArApiCompat.Utilities.AsmResolver; using AsmResolver.DotNet; -using CompatUnbreaker.Tool.ApiCompatibility.AssemblyMapping; -using CompatUnbreaker.Tool.Utilities.AsmResolver; -namespace CompatUnbreaker.Tool.ApiCompatibility.Comparing.Rules; - -public sealed class CannotChangeGenericConstraintDifference(DifferenceType type, IMemberDefinition left, IMemberDefinition right, GenericParameter leftTypeParameter, string constraint) : CompatDifference +namespace ArApiCompat.ApiCompatibility.Comparing.Rules; + +#pragma warning disable CS9113 // Parameter is unread. +public sealed class CannotChangeGenericConstraintDifference(DifferenceType type, IMemberDefinition left, IMemberDefinition right, GenericParameter leftTypeParameter, string constraint) : CompatDifference { public override string Message => $"Cannot {(type == DifferenceType.Added ? "add" : "remove")} constraint '{constraint}' on type parameter '{leftTypeParameter}' of '{left}'"; public override DifferenceType Type => type; -} +} +#pragma warning restore CS9113 // Parameter is unread. public sealed class CannotChangeGenericConstraints : BaseRule { @@ -44,7 +46,7 @@ public override void Run(MemberMapper mapper, IList difference CompareTypeParameters(leftTypeParameters, rightTypeParameters, left, right, permitConstraintRemoval, differences); } - private void CompareTypeParameters( + private static void CompareTypeParameters( IList leftTypeParameters, IList rightTypeParameters, IMemberDefinition left, diff --git a/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotChangeVisibility.cs b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotChangeVisibility.cs index 49debdf..b4d31d4 100644 --- a/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotChangeVisibility.cs +++ b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotChangeVisibility.cs @@ -1,11 +1,9 @@ +using ArApiCompat.ApiCompatibility.AssemblyMapping; +using ArApiCompat.Utilities.AsmResolver; using AsmResolver.DotNet; -using CompatUnbreaker.Tool.ApiCompatibility.AssemblyMapping; -using CompatUnbreaker.Tool.Utilities.AsmResolver; -using CompatUnbreaker.Utilities.AsmResolver; -//using Microsoft.CodeAnalysis; -using Accessibility = CompatUnbreaker.Utilities.AsmResolver.Accessibility; +using Accessibility = ArApiCompat.Utilities.AsmResolver.Accessibility; -namespace CompatUnbreaker.Tool.ApiCompatibility.Comparing.Rules; +namespace ArApiCompat.ApiCompatibility.Comparing.Rules; public sealed class CannotReduceVisibilityDifference(IMemberDefinition left, IMemberDefinition right) : CompatDifference { diff --git a/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotRemoveBaseTypeOrInterface.cs b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotRemoveBaseTypeOrInterface.cs index acae5e8..e46fe5e 100644 --- a/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotRemoveBaseTypeOrInterface.cs +++ b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotRemoveBaseTypeOrInterface.cs @@ -1,9 +1,9 @@ +using ArApiCompat.ApiCompatibility.AssemblyMapping; +using ArApiCompat.Utilities.AsmResolver; using AsmResolver.DotNet; -using CompatUnbreaker.Tool.ApiCompatibility.AssemblyMapping; using CompatUnbreaker.Tool.Utilities.AsmResolver; -using CompatUnbreaker.Utilities.AsmResolver; -namespace CompatUnbreaker.Tool.ApiCompatibility.Comparing.Rules; +namespace ArApiCompat.ApiCompatibility.Comparing.Rules; public sealed class CannotRemoveBaseTypeDifference(TypeMapper mapper) : TypeCompatDifference(mapper) { @@ -37,7 +37,7 @@ public override void Run(TypeMapper mapper, IList differences) ValidateInterfaceNotRemoved(mapper, differences); } - private void ValidateBaseTypeNotRemoved(TypeMapper mapper, IList differences) + private static void ValidateBaseTypeNotRemoved(TypeMapper mapper, IList differences) { var (left, right) = mapper; @@ -64,7 +64,7 @@ private void ValidateBaseTypeNotRemoved(TypeMapper mapper, IList differences) + private static void ValidateInterfaceNotRemoved(TypeMapper mapper, IList differences) { var (left, right) = mapper; diff --git a/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotSealType.cs b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotSealType.cs index 736733c..ac64e62 100644 --- a/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotSealType.cs +++ b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotSealType.cs @@ -1,8 +1,7 @@ -using CompatUnbreaker.Tool.ApiCompatibility.AssemblyMapping; -using CompatUnbreaker.Tool.Utilities.AsmResolver; -using CompatUnbreaker.Utilities.AsmResolver; +using ArApiCompat.ApiCompatibility.AssemblyMapping; +using ArApiCompat.Utilities.AsmResolver; -namespace CompatUnbreaker.Tool.ApiCompatibility.Comparing.Rules; +namespace ArApiCompat.ApiCompatibility.Comparing.Rules; public sealed class CannotSealTypeDifference(TypeMapper mapper) : TypeCompatDifference(mapper) { diff --git a/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/EnumsMustMatch.cs b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/EnumsMustMatch.cs index 47fabb8..12993bd 100644 --- a/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/EnumsMustMatch.cs +++ b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/EnumsMustMatch.cs @@ -1,8 +1,8 @@ +using ArApiCompat.ApiCompatibility.AssemblyMapping; +using ArApiCompat.Utilities.AsmResolver; using AsmResolver.DotNet; -using CompatUnbreaker.Tool.ApiCompatibility.AssemblyMapping; -using CompatUnbreaker.Tool.Utilities.AsmResolver; -namespace CompatUnbreaker.Tool.ApiCompatibility.Comparing.Rules; +namespace ArApiCompat.ApiCompatibility.Comparing.Rules; public sealed class EnumTypesMustMatch(TypeMapper mapper) : TypeCompatDifference(mapper) { diff --git a/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/MembersMustExist.cs b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/MembersMustExist.cs index 8535762..8ca2b8e 100644 --- a/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/MembersMustExist.cs +++ b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/MembersMustExist.cs @@ -1,8 +1,9 @@ +using ArApiCompat.ApiCompatibility.AssemblyMapping; +using ArApiCompat.Utilities.AsmResolver; using AsmResolver.DotNet; -using CompatUnbreaker.Tool.ApiCompatibility.AssemblyMapping; using CompatUnbreaker.Tool.Utilities.AsmResolver; -namespace CompatUnbreaker.Tool.ApiCompatibility.Comparing.Rules; +namespace ArApiCompat.ApiCompatibility.Comparing.Rules; public sealed class TypeMustExistDifference(TypeMapper mapper) : TypeCompatDifference(mapper) { diff --git a/src/build/ArApiCompat/ArApiCompat.csproj b/src/build/ArApiCompat/ArApiCompat.csproj index 08d27ea..f614953 100644 --- a/src/build/ArApiCompat/ArApiCompat.csproj +++ b/src/build/ArApiCompat/ArApiCompat.csproj @@ -4,6 +4,7 @@ net9.0 enable enable + $(NoWarn);CA1043;CA1028 diff --git a/src/build/ArApiCompat/Utilities/AsmResolver/Accessibility.cs b/src/build/ArApiCompat/Utilities/AsmResolver/Accessibility.cs index ebc0c5c..b76205c 100644 --- a/src/build/ArApiCompat/Utilities/AsmResolver/Accessibility.cs +++ b/src/build/ArApiCompat/Utilities/AsmResolver/Accessibility.cs @@ -4,7 +4,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace CompatUnbreaker.Utilities.AsmResolver; +namespace ArApiCompat.Utilities.AsmResolver; internal enum Accessibility { diff --git a/src/build/ArApiCompat/Utilities/AsmResolver/AccessibilityExtensions.cs b/src/build/ArApiCompat/Utilities/AsmResolver/AccessibilityExtensions.cs index 9ab1a4f..d1b7338 100644 --- a/src/build/ArApiCompat/Utilities/AsmResolver/AccessibilityExtensions.cs +++ b/src/build/ArApiCompat/Utilities/AsmResolver/AccessibilityExtensions.cs @@ -1,7 +1,7 @@ using AsmResolver.DotNet; using AsmResolver.PE.DotNet.Metadata.Tables; -namespace CompatUnbreaker.Utilities.AsmResolver; +namespace ArApiCompat.Utilities.AsmResolver; internal static class AccessibilityExtensions { @@ -30,7 +30,7 @@ public static Accessibility GetAccessibility(this TypeDefinition type) TypeAttributes.NestedAssembly => Accessibility.Internal, TypeAttributes.NestedFamilyAndAssembly => Accessibility.ProtectedAndInternal, TypeAttributes.NestedFamilyOrAssembly => Accessibility.ProtectedOrInternal, - _ => throw new Exception(), + _ => throw new ArgumentOutOfRangeException(nameof(type)), }; } @@ -44,7 +44,7 @@ public static Accessibility GetAccessibility(this MethodDefinition method) MethodAttributes.Family => Accessibility.Protected, MethodAttributes.FamilyOrAssembly => Accessibility.ProtectedOrInternal, MethodAttributes.Public => Accessibility.Public, - _ => throw new Exception(), + _ => throw new ArgumentOutOfRangeException(nameof(method)), }; } @@ -58,7 +58,7 @@ public static Accessibility GetAccessibility(this FieldDefinition field) FieldAttributes.Family => Accessibility.Protected, FieldAttributes.FamilyOrAssembly => Accessibility.ProtectedOrInternal, FieldAttributes.Public => Accessibility.Public, - _ => throw new Exception(), + _ => throw new ArgumentOutOfRangeException(nameof(field)), }; } diff --git a/src/build/ArApiCompat/Utilities/AsmResolver/DefinitionModifiersExtensions.cs b/src/build/ArApiCompat/Utilities/AsmResolver/DefinitionModifiersExtensions.cs index ccfd58c..86c4d81 100644 --- a/src/build/ArApiCompat/Utilities/AsmResolver/DefinitionModifiersExtensions.cs +++ b/src/build/ArApiCompat/Utilities/AsmResolver/DefinitionModifiersExtensions.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using System.Text.RegularExpressions; +using ArApiCompat.Utilities.AsmResolver; using AsmResolver.DotNet; namespace CompatUnbreaker.Tool.Utilities.AsmResolver; diff --git a/src/build/ArApiCompat/Utilities/AsmResolver/ExtendedSignatureComparer.cs b/src/build/ArApiCompat/Utilities/AsmResolver/ExtendedSignatureComparer.cs index cb15ebe..6c9ba7e 100644 --- a/src/build/ArApiCompat/Utilities/AsmResolver/ExtendedSignatureComparer.cs +++ b/src/build/ArApiCompat/Utilities/AsmResolver/ExtendedSignatureComparer.cs @@ -2,7 +2,7 @@ using AsmResolver.DotNet; using AsmResolver.DotNet.Signatures; -namespace CompatUnbreaker.Tool.Utilities.AsmResolver; +namespace ArApiCompat.Utilities.AsmResolver; // TODO upstream? diff --git a/src/build/ArApiCompat/Utilities/AsmResolver/GenericParameterExtensions.cs b/src/build/ArApiCompat/Utilities/AsmResolver/GenericParameterExtensions.cs index 4155d3b..236a82a 100644 --- a/src/build/ArApiCompat/Utilities/AsmResolver/GenericParameterExtensions.cs +++ b/src/build/ArApiCompat/Utilities/AsmResolver/GenericParameterExtensions.cs @@ -1,6 +1,6 @@ using AsmResolver.DotNet; -namespace CompatUnbreaker.Tool.Utilities.AsmResolver; +namespace ArApiCompat.Utilities.AsmResolver; internal static class GenericParameterExtensions { diff --git a/src/build/ArApiCompat/Utilities/AsmResolver/MemberDefinitionExtensions.cs b/src/build/ArApiCompat/Utilities/AsmResolver/MemberDefinitionExtensions.cs index 95172ee..9c50b8f 100644 --- a/src/build/ArApiCompat/Utilities/AsmResolver/MemberDefinitionExtensions.cs +++ b/src/build/ArApiCompat/Utilities/AsmResolver/MemberDefinitionExtensions.cs @@ -1,6 +1,6 @@ using AsmResolver.DotNet; -namespace CompatUnbreaker.Tool.Utilities.AsmResolver; +namespace ArApiCompat.Utilities.AsmResolver; internal static class MemberDefinitionExtensions { diff --git a/src/build/ArApiCompat/Utilities/AsmResolver/MethodDefinitionExtensions.cs b/src/build/ArApiCompat/Utilities/AsmResolver/MethodDefinitionExtensions.cs index 080958d..f436834 100644 --- a/src/build/ArApiCompat/Utilities/AsmResolver/MethodDefinitionExtensions.cs +++ b/src/build/ArApiCompat/Utilities/AsmResolver/MethodDefinitionExtensions.cs @@ -2,7 +2,7 @@ using AsmResolver.DotNet.Signatures; using AsmResolver.PE.DotNet.Metadata.Tables; -namespace CompatUnbreaker.Tool.Utilities.AsmResolver; +namespace ArApiCompat.Utilities.AsmResolver; internal static class MethodDefinitionExtensions { diff --git a/src/build/ArApiCompat/Utilities/AsmResolver/MiscellaneousExtensions.cs b/src/build/ArApiCompat/Utilities/AsmResolver/MiscellaneousExtensions.cs index 60d8de2..0e0dfdd 100644 --- a/src/build/ArApiCompat/Utilities/AsmResolver/MiscellaneousExtensions.cs +++ b/src/build/ArApiCompat/Utilities/AsmResolver/MiscellaneousExtensions.cs @@ -1,6 +1,6 @@ using AsmResolver.PE.DotNet.Cil; -namespace CompatUnbreaker.Tool.Utilities.AsmResolver; +namespace ArApiCompat.Utilities.AsmResolver; internal static class MiscellaneousExtensions { diff --git a/src/build/ArApiCompat/Utilities/AsmResolver/TypeDefinitionExtensions.InterfaceMapping.cs b/src/build/ArApiCompat/Utilities/AsmResolver/TypeDefinitionExtensions.InterfaceMapping.cs index ec05e14..ea3478d 100644 --- a/src/build/ArApiCompat/Utilities/AsmResolver/TypeDefinitionExtensions.InterfaceMapping.cs +++ b/src/build/ArApiCompat/Utilities/AsmResolver/TypeDefinitionExtensions.InterfaceMapping.cs @@ -1,3 +1,4 @@ +using ArApiCompat.Utilities.AsmResolver; using AsmResolver.DotNet; namespace CompatUnbreaker.Tool.Utilities.AsmResolver; diff --git a/src/build/ArApiCompat/Utilities/AsmResolver/TypeDefinitionExtensions.cs b/src/build/ArApiCompat/Utilities/AsmResolver/TypeDefinitionExtensions.cs index 19eefd1..f5ef4ee 100644 --- a/src/build/ArApiCompat/Utilities/AsmResolver/TypeDefinitionExtensions.cs +++ b/src/build/ArApiCompat/Utilities/AsmResolver/TypeDefinitionExtensions.cs @@ -1,3 +1,4 @@ +using ArApiCompat.Utilities; using AsmResolver.DotNet; namespace CompatUnbreaker.Tool.Utilities.AsmResolver; diff --git a/src/build/ArApiCompat/Utilities/AsmResolver/TypeSignatureExtensions.cs b/src/build/ArApiCompat/Utilities/AsmResolver/TypeSignatureExtensions.cs index e3ec4e7..41ee310 100644 --- a/src/build/ArApiCompat/Utilities/AsmResolver/TypeSignatureExtensions.cs +++ b/src/build/ArApiCompat/Utilities/AsmResolver/TypeSignatureExtensions.cs @@ -1,7 +1,7 @@ using AsmResolver.DotNet; using AsmResolver.DotNet.Signatures; -namespace CompatUnbreaker.Tool.Utilities.AsmResolver; +namespace ArApiCompat.Utilities.AsmResolver; internal static class TypeSignatureExtensions { diff --git a/src/build/ArApiCompat/Utilities/AsmResolver/VisibilityExtensions.cs b/src/build/ArApiCompat/Utilities/AsmResolver/VisibilityExtensions.cs index 122c553..3367382 100644 --- a/src/build/ArApiCompat/Utilities/AsmResolver/VisibilityExtensions.cs +++ b/src/build/ArApiCompat/Utilities/AsmResolver/VisibilityExtensions.cs @@ -1,6 +1,6 @@ using AsmResolver.DotNet; -namespace CompatUnbreaker.Utilities.AsmResolver; +namespace ArApiCompat.Utilities.AsmResolver; internal static class VisibilityExtensions { diff --git a/src/build/ArApiCompat/Utilities/MetadataHelpers.cs b/src/build/ArApiCompat/Utilities/MetadataHelpers.cs index 87be013..4706efe 100644 --- a/src/build/ArApiCompat/Utilities/MetadataHelpers.cs +++ b/src/build/ArApiCompat/Utilities/MetadataHelpers.cs @@ -1,6 +1,6 @@ using System.Diagnostics; -namespace CompatUnbreaker.Tool.Utilities; +namespace ArApiCompat.Utilities; // Copied from https://github.com/dotnet/roslyn/blob/7c625024a1984d9f04f317940d518402f5898758/src/Compilers/Core/Portable/MetadataReader/MetadataHelpers.cs diff --git a/src/build/ArApiCompat/Utilities/StringExtensions.cs b/src/build/ArApiCompat/Utilities/StringExtensions.cs index e588f73..d604590 100644 --- a/src/build/ArApiCompat/Utilities/StringExtensions.cs +++ b/src/build/ArApiCompat/Utilities/StringExtensions.cs @@ -1,4 +1,4 @@ -namespace CompatUnbreaker.Tool.Utilities; +namespace ArApiCompat.Utilities; internal static class StringExtensions { From dc969628ac30beadb5d2294745acf8fae6a29d35 Mon Sep 17 00:00:00 2001 From: DaNike Date: Wed, 5 Nov 2025 23:54:57 -0600 Subject: [PATCH 10/15] Implement basic single-assembly comparison --- .../MonoMod.Backports.Shims.csproj | 3 +- .../AssemblyMapping/AssemblyMapper.cs | 128 +++++---- .../ApiCompatibility/Comparing/ApiComparer.cs | 4 +- .../Rules/CannotChangeGenericConstraints.cs | 254 +++++++++--------- .../Suppressions/SuppressionFile.cs | 203 ++++++++++++++ src/build/ArApiCompat/ArApiCompat.csproj | 2 + src/build/ArApiCompat/Program.cs | 146 ++++++++++ 7 files changed, 544 insertions(+), 196 deletions(-) create mode 100644 src/build/ArApiCompat/ApiCompatibility/Suppressions/SuppressionFile.cs create mode 100644 src/build/ArApiCompat/Program.cs diff --git a/src/MonoMod.Backports.Shims/MonoMod.Backports.Shims.csproj b/src/MonoMod.Backports.Shims/MonoMod.Backports.Shims.csproj index cd284e1..d9e8431 100644 --- a/src/MonoMod.Backports.Shims/MonoMod.Backports.Shims.csproj +++ b/src/MonoMod.Backports.Shims/MonoMod.Backports.Shims.csproj @@ -1,4 +1,4 @@ - + true @@ -41,5 +41,4 @@ - diff --git a/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/AssemblyMapper.cs b/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/AssemblyMapper.cs index 34ab791..a711143 100644 --- a/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/AssemblyMapper.cs +++ b/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/AssemblyMapper.cs @@ -1,65 +1,63 @@ -using ArApiCompat.Utilities.AsmResolver; -using AsmResolver.DotNet; - -namespace ArApiCompat.ApiCompatibility.AssemblyMapping; - -public sealed class AssemblyMapper(MapperSettings MapperSettings) : ElementMapper(MapperSettings) -{ - private readonly Dictionary _types = new(ExtendedSignatureComparer.VersionAgnostic); - - public IEnumerable Types => _types.Values; - - public override void Add(AssemblyDefinition value, ElementSide side) - { - base.Add(value, side); - - foreach (var module in value.Modules) - { - foreach (var type in module.TopLevelTypes) - { - if (type.Namespace is null) continue; - - if (MapperSettings.Filter(type)) - { - AddOrCreateMapper(type, side); - } - } - - foreach (var exportedType in module.ExportedTypes) - { - var type = exportedType.Resolve(); - if (type == null) - { - Console.WriteLine($"Failed to resolve exported type: {exportedType.FullName}"); - return; - } - - if (MapperSettings.Filter(type)) - { - AddOrCreateMapper(type, side); - } - } - } - } - - private void AddOrCreateMapper(TypeDefinition type, ElementSide side) - { - if (!_types.TryGetValue(type, out var mapper)) - { - mapper = new TypeMapper(MapperSettings); - _types.Add(type, mapper); - } - - mapper.Add(type, side); - } - - public static AssemblyMapper Create(AssemblyDefinition left, AssemblyDefinition right, MapperSettings? settings = null) - { - var result = new AssemblyMapper(settings ?? new MapperSettings()); - - result.Add(left, ElementSide.Left); - result.Add(right, ElementSide.Right); - - return result; - } -} +using ArApiCompat.Utilities.AsmResolver; +using AsmResolver.DotNet; + +namespace ArApiCompat.ApiCompatibility.AssemblyMapping; + +public sealed class AssemblyMapper(MapperSettings MapperSettings) : ElementMapper(MapperSettings) +{ + private readonly Dictionary _types = new(ExtendedSignatureComparer.VersionAgnostic); + + public IEnumerable Types => _types.Values; + + public override void Add(AssemblyDefinition value, ElementSide side) + { + base.Add(value, side); + + foreach (var module in value.Modules) + { + foreach (var type in module.TopLevelTypes) + { + if (MapperSettings.Filter(type)) + { + AddOrCreateMapper(type, side); + } + } + + foreach (var exportedType in module.ExportedTypes) + { + var type = exportedType.Resolve(); + if (type == null) + { + Console.WriteLine($"Failed to resolve exported type: {exportedType.FullName}"); + return; + } + + if (MapperSettings.Filter(type)) + { + AddOrCreateMapper(type, side); + } + } + } + } + + private void AddOrCreateMapper(TypeDefinition type, ElementSide side) + { + if (!_types.TryGetValue(type, out var mapper)) + { + mapper = new TypeMapper(MapperSettings); + _types.Add(type, mapper); + } + + mapper.Add(type, side); + } + + public static AssemblyMapper Create(AssemblyDefinition left, AssemblyDefinition right, MapperSettings? settings = null) + { + var result = new AssemblyMapper(settings ?? new MapperSettings()); + + result.Add(left, ElementSide.Left); + result.Add(right, ElementSide.Right); + + return result; + } +} diff --git a/src/build/ArApiCompat/ApiCompatibility/Comparing/ApiComparer.cs b/src/build/ArApiCompat/ApiCompatibility/Comparing/ApiComparer.cs index a8ea03f..805aa7e 100644 --- a/src/build/ArApiCompat/ApiCompatibility/Comparing/ApiComparer.cs +++ b/src/build/ArApiCompat/ApiCompatibility/Comparing/ApiComparer.cs @@ -24,8 +24,8 @@ public sealed class ApiComparer public IEnumerable CompatDifferences => _compatDifferences; public void Compare(AssemblyMapper assemblyMapper) - { - ArgumentNullException.ThrowIfNull(assemblyMapper); + { + ArgumentNullException.ThrowIfNull(assemblyMapper); AddDifferences(assemblyMapper); diff --git a/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotChangeGenericConstraints.cs b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotChangeGenericConstraints.cs index 9e5a7bd..c8b9774 100644 --- a/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotChangeGenericConstraints.cs +++ b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotChangeGenericConstraints.cs @@ -1,132 +1,132 @@ -using System.Diagnostics; -using ArApiCompat.ApiCompatibility.AssemblyMapping; -using ArApiCompat.Utilities.AsmResolver; -using AsmResolver.DotNet; - +using System.Diagnostics; +using ArApiCompat.ApiCompatibility.AssemblyMapping; +using ArApiCompat.Utilities.AsmResolver; +using AsmResolver.DotNet; + namespace ArApiCompat.ApiCompatibility.Comparing.Rules; #pragma warning disable CS9113 // Parameter is unread. public sealed class CannotChangeGenericConstraintDifference(DifferenceType type, IMemberDefinition left, IMemberDefinition right, GenericParameter leftTypeParameter, string constraint) : CompatDifference -{ - public override string Message => $"Cannot {(type == DifferenceType.Added ? "add" : "remove")} constraint '{constraint}' on type parameter '{leftTypeParameter}' of '{left}'"; - public override DifferenceType Type => type; +{ + public override string Message => $"Cannot {(type == DifferenceType.Added ? "add" : "remove")} constraint '{constraint}' on type parameter '{leftTypeParameter}' of '{left}'"; + public override DifferenceType Type => type; +} +#pragma warning restore CS9113 // Parameter is unread. + +public sealed class CannotChangeGenericConstraints : BaseRule +{ + public override void Run(TypeMapper mapper, IList differences) + { + var (left, right) = mapper; + if (left == null || right == null) + return; + + var leftTypeParameters = left.GenericParameters; + var rightTypeParameters = right.GenericParameters; + + // can remove constraints on sealed classes since no code should observe broader set of type parameters + var permitConstraintRemoval = left.IsSealed; + + CompareTypeParameters(leftTypeParameters, rightTypeParameters, left, right, permitConstraintRemoval, differences); + } + + public override void Run(MemberMapper mapper, IList differences) + { + var (left, right) = mapper; + if (left is not MethodDefinition leftMethod || right is not MethodDefinition rightMethod) + { + return; + } + + var leftTypeParameters = leftMethod.GenericParameters; + var rightTypeParameters = rightMethod.GenericParameters; + + var permitConstraintRemoval = !leftMethod.IsVirtual; + + CompareTypeParameters(leftTypeParameters, rightTypeParameters, left, right, permitConstraintRemoval, differences); + } + + private static void CompareTypeParameters( + IList leftTypeParameters, + IList rightTypeParameters, + IMemberDefinition left, + IMemberDefinition right, + bool permitConstraintRemoval, + IList differences + ) + { + Debug.Assert(leftTypeParameters.Count == rightTypeParameters.Count); + for (var i = 0; i < leftTypeParameters.Count; i++) + { + var leftTypeParam = leftTypeParameters[i]; + var rightTypeParam = rightTypeParameters[i]; + + var addedConstraints = new List(); + var removedConstraints = new List(); + + // CompareBoolConstraint(typeParam => typeParam.HasConstructorConstraint, "new()"); TODO + // CompareBoolConstraint(typeParam => typeParam.HasNotNullConstraint, "notnull"); TODO + CompareBoolConstraint(typeParam => typeParam.HasReferenceTypeConstraint, "class"); + // CompareBoolConstraint(typeParam => typeParam.HasUnmanagedTypeConstraint, "unmanaged"); TODO + // unmanaged implies struct + // CompareBoolConstraint(typeParam => typeParam.HasValueTypeConstraint & !typeParam.HasUnmanagedTypeConstraint, "struct"); TODO + + var rightOnlyConstraints = ToHashSet(rightTypeParam.Constraints); + rightOnlyConstraints.ExceptWith(ToHashSet(leftTypeParam.Constraints)); + + // we could allow an addition if removals are allowed, and the addition is a less-derived base type or interface + // for example: changing a constraint from MemoryStream to Stream on a sealed type, or non-virtual member + // but we'll leave this to suppressions + + addedConstraints.AddRange(rightOnlyConstraints.Where(x => x is not null).Select(x => x!.FullName)); + + // additions + foreach (var addedConstraint in addedConstraints) + { + differences.Add(new CannotChangeGenericConstraintDifference(DifferenceType.Added, left, right, leftTypeParam, addedConstraint)); + } + + // removals + // we could allow a removal in the case of reducing to more-derived interfaces if those interfaces were previous constraints + // for example if IB : IA and a type is constrained by both IA and IB, it's safe to remove IA since it's implied by IB + // but we'll leave this to suppressions + + if (!permitConstraintRemoval) + { + var leftOnlyConstraints = ToHashSet(leftTypeParam.Constraints); + leftOnlyConstraints.ExceptWith(ToHashSet(rightTypeParam.Constraints)); + removedConstraints.AddRange(leftOnlyConstraints.Where(x => x is not null).Select(x => x!.FullName)); + + foreach (var removedConstraint in removedConstraints) + { + differences.Add(new CannotChangeGenericConstraintDifference(DifferenceType.Removed, left, right, leftTypeParam, removedConstraint)); + } + } + + void CompareBoolConstraint(Func boolConstraint, string constraintName) + { + var leftBoolConstraint = boolConstraint(leftTypeParam); + var rightBoolConstraint = boolConstraint(rightTypeParam); + + // addition + if (!leftBoolConstraint && rightBoolConstraint) + { + addedConstraints.Add(constraintName); + } + // removal + else if (!permitConstraintRemoval && leftBoolConstraint && !rightBoolConstraint) + { + removedConstraints.Add(constraintName); + } + } + } + } + + private static HashSet ToHashSet(IList constraints) + { + return constraints + .Select(c => c.Constraint) + .Where(c => c != null) + .ToHashSet(ExtendedSignatureComparer.VersionAgnostic!); + } } -#pragma warning restore CS9113 // Parameter is unread. - -public sealed class CannotChangeGenericConstraints : BaseRule -{ - public override void Run(TypeMapper mapper, IList differences) - { - var (left, right) = mapper; - if (left == null || right == null) - return; - - var leftTypeParameters = left.GenericParameters; - var rightTypeParameters = right.GenericParameters; - - // can remove constraints on sealed classes since no code should observe broader set of type parameters - var permitConstraintRemoval = left.IsSealed; - - CompareTypeParameters(leftTypeParameters, rightTypeParameters, left, right, permitConstraintRemoval, differences); - } - - public override void Run(MemberMapper mapper, IList differences) - { - var (left, right) = mapper; - if (left is not MethodDefinition leftMethod || right is not MethodDefinition rightMethod) - { - return; - } - - var leftTypeParameters = leftMethod.GenericParameters; - var rightTypeParameters = rightMethod.GenericParameters; - - var permitConstraintRemoval = !leftMethod.IsVirtual; - - CompareTypeParameters(leftTypeParameters, rightTypeParameters, left, right, permitConstraintRemoval, differences); - } - - private static void CompareTypeParameters( - IList leftTypeParameters, - IList rightTypeParameters, - IMemberDefinition left, - IMemberDefinition right, - bool permitConstraintRemoval, - IList differences - ) - { - Debug.Assert(leftTypeParameters.Count == rightTypeParameters.Count); - for (var i = 0; i < leftTypeParameters.Count; i++) - { - var leftTypeParam = leftTypeParameters[i]; - var rightTypeParam = rightTypeParameters[i]; - - var addedConstraints = new List(); - var removedConstraints = new List(); - - // CompareBoolConstraint(typeParam => typeParam.HasConstructorConstraint, "new()"); TODO - // CompareBoolConstraint(typeParam => typeParam.HasNotNullConstraint, "notnull"); TODO - CompareBoolConstraint(typeParam => typeParam.HasReferenceTypeConstraint, "class"); - // CompareBoolConstraint(typeParam => typeParam.HasUnmanagedTypeConstraint, "unmanaged"); TODO - // unmanaged implies struct - // CompareBoolConstraint(typeParam => typeParam.HasValueTypeConstraint & !typeParam.HasUnmanagedTypeConstraint, "struct"); TODO - - var rightOnlyConstraints = ToHashSet(rightTypeParam.Constraints); - rightOnlyConstraints.ExceptWith(ToHashSet(leftTypeParam.Constraints)); - - // we could allow an addition if removals are allowed, and the addition is a less-derived base type or interface - // for example: changing a constraint from MemoryStream to Stream on a sealed type, or non-virtual member - // but we'll leave this to suppressions - - addedConstraints.AddRange(rightOnlyConstraints.Where(x => x is not null).Select(x => x!.FullName)); - - // additions - foreach (var addedConstraint in addedConstraints) - { - differences.Add(new CannotChangeGenericConstraintDifference(DifferenceType.Added, left, right, leftTypeParam, addedConstraint)); - } - - // removals - // we could allow a removal in the case of reducing to more-derived interfaces if those interfaces were previous constraints - // for example if IB : IA and a type is constrained by both IA and IB, it's safe to remove IA since it's implied by IB - // but we'll leave this to suppressions - - if (!permitConstraintRemoval) - { - var leftOnlyConstraints = ToHashSet(leftTypeParam.Constraints); - leftOnlyConstraints.ExceptWith(ToHashSet(rightTypeParam.Constraints)); - removedConstraints.AddRange(leftOnlyConstraints.Where(x => x is not null).Select(x => x!.FullName)); - - foreach (var removedConstraint in removedConstraints) - { - differences.Add(new CannotChangeGenericConstraintDifference(DifferenceType.Removed, left, right, leftTypeParam, removedConstraint)); - } - } - - void CompareBoolConstraint(Func boolConstraint, string constraintName) - { - var leftBoolConstraint = boolConstraint(leftTypeParam); - var rightBoolConstraint = boolConstraint(rightTypeParam); - - // addition - if (!leftBoolConstraint && rightBoolConstraint) - { - addedConstraints.Add(constraintName); - } - // removal - else if (!permitConstraintRemoval && leftBoolConstraint && !rightBoolConstraint) - { - removedConstraints.Add(constraintName); - } - } - } - } - - private static HashSet ToHashSet(IList constraints) - { - return constraints - .Select(c => c.Constraint) - .Where(c => c != null) - .ToHashSet(ExtendedSignatureComparer.VersionAgnostic!); - } -} diff --git a/src/build/ArApiCompat/ApiCompatibility/Suppressions/SuppressionFile.cs b/src/build/ArApiCompat/ApiCompatibility/Suppressions/SuppressionFile.cs new file mode 100644 index 0000000..f5dd034 --- /dev/null +++ b/src/build/ArApiCompat/ApiCompatibility/Suppressions/SuppressionFile.cs @@ -0,0 +1,203 @@ +using ArApiCompat.ApiCompatibility.Comparing; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Linq; + +namespace ArApiCompat.ApiCompatibility.Suppressions; + +internal sealed class SuppressionFile +{ + + public sealed class Comparison + { + public string? Left { get; set; } + public string? Right { get; set; } + + public List Suppressions { get; } = new(); + } + + public sealed record Suppression + { + public DifferenceType DifferenceType { get; set; } + public string? TypeName { get; set; } + public string? Message { get; set; } + } + + public List Comparisons { get; } = new(); + + public Comparison? GetComparison(string? left, string? right) + => Comparisons.FirstOrDefault(c => c.Left == left && c.Right == right); + + public void Sort() + { + Comparisons.Sort((a, b) + => StringComparer.Ordinal.Compare(a.Left, b.Left) * 2 + + StringComparer.Ordinal.Compare(a.Right, b.Right)); + + foreach (var comparison in Comparisons) + { + comparison.Suppressions.Sort((a, b) + => a.DifferenceType.CompareTo(b.DifferenceType) * 4 + + StringComparer.Ordinal.Compare(a.TypeName, b.TypeName) * 2 + + StringComparer.Ordinal.Compare(a.Message, b.Message)); + } + } + + public SuppressionFile RemoveSuppressionsFrom(SuppressionFile other, out bool otherHasUnusedSuppressions) + { + var usedSuppressions = new HashSet(ReferenceEqualityComparer.Instance); + + var result = new SuppressionFile(); + foreach (var comparison in Comparisons) + { + var newComparison = new Comparison() + { + Left = comparison.Left, + Right = comparison.Right, + }; + + var otherComparison = other.GetComparison(comparison.Left, comparison.Right); + if (otherComparison is null) + { + // no matching comparison in suppression source, add directly + newComparison.Suppressions.AddRange(comparison.Suppressions.Select(s => s with { })); + } + else + { + // there was a matching comparison, go suppression-by-suppression to compare + foreach (var suppression in comparison.Suppressions) + { + var matching = otherComparison.Suppressions.FirstOrDefault(c => c == suppression); + if (matching is null) + { + // there's no matching suppression in other, add a clone + newComparison.Suppressions.Add(suppression with { }); + } + else + { + // there's a matching suppression in other, mark it + _ = usedSuppressions.Add(matching); + } + } + } + + if (newComparison.Suppressions.Count > 0) + { + result.Comparisons.Add(newComparison); + } + } + + otherHasUnusedSuppressions = other + .Comparisons.SelectMany(c => c.Suppressions) + .Any(s => !usedSuppressions.Contains(s)); + + return result; + } + + private static readonly XName NArCompatSuppressions = "ArCompatSuppressions"; + private static readonly XName NComparison = "Comparison"; + private static readonly XName NLeft = "Left"; + private static readonly XName NRight = "Right"; + private static readonly XName NSuppression = "Suppression"; + private static readonly XName NDifferenceType = "DifferenceType"; + private static readonly XName NTypeName = "TypeName"; + private static readonly XName NMessage = "Message"; + + public XDocument Serialize() + { + var sortedComparisons = Comparisons + .OrderBy(c => c.Left) + .ThenBy(c => c.Right); + + var doc = new XDocument(); + var rootNode = new XElement(NArCompatSuppressions); + doc.Add(rootNode); + + foreach (var comparison in sortedComparisons) + { + var compareNode = new XElement(NComparison); + rootNode.Add(compareNode); + if (comparison.Left != null) + { + compareNode.Add(new XAttribute(NLeft, comparison.Left)); + } + if (comparison.Right != null) + { + compareNode.Add(new XAttribute(NRight, comparison.Right)); + } + + var suppressions = comparison.Suppressions + .OrderBy(s => s.DifferenceType) + .ThenBy(s => s.TypeName) + .ThenBy(s => s.Message); + + foreach (var suppression in suppressions) + { + var suppressionNode = new XElement(NSuppression, + new XAttribute(NDifferenceType, suppression.DifferenceType.ToString()), + new XElement(NTypeName, suppression.TypeName)); + if (suppression.Message is not null) + { + suppressionNode.Add(new XElement(NMessage, new XText(suppression.Message))); + } + compareNode.Add(suppressionNode); + } + } + + return doc; + } + + public static SuppressionFile Deserialize(XDocument document) + { + [DoesNotReturn] + static void Throw(string message) => throw new InvalidOperationException(message); + + var root = document.Root; + if (root is null) + Throw("Suppressions file must have root node"); + if (root.Name != NArCompatSuppressions) + Throw($"Suppressions file root must be '{NArCompatSuppressions}'"); + + var result = new SuppressionFile(); + foreach (var child in root.Elements()) + { + if (child.Name != NComparison) + Throw($"Children of root must be '{NComparison}'"); + + var comparison = new Comparison(); + if (child.Attribute(NLeft) is { } attrl) + comparison.Left = attrl.Value; + if (child.Attribute(NRight) is { } attrr) + comparison.Right = attrr.Value; + + foreach (var child2 in child.Elements()) + { + if (child2.Name != NSuppression) + Throw($"Children of '{NComparison}' must be '{NSuppression}'"); + if (child2.Attribute(NDifferenceType) is not { } attrDiffType) + Throw($"'{NSuppression}' must have attribute '{NDifferenceType}'"); + + var suppression = new Suppression(); + suppression.DifferenceType = Enum.Parse(attrDiffType.Value); + if (child2.Element(NTypeName) is { } typeNameElem) + { + suppression.TypeName = typeNameElem.Value; + } + if (child2.Element(NMessage) is { } messageElem) + { + suppression.Message = messageElem.Value; + } + + comparison.Suppressions.Add(suppression); + } + + result.Comparisons.Add(comparison); + } + + return result; + } +} diff --git a/src/build/ArApiCompat/ArApiCompat.csproj b/src/build/ArApiCompat/ArApiCompat.csproj index f614953..09708e0 100644 --- a/src/build/ArApiCompat/ArApiCompat.csproj +++ b/src/build/ArApiCompat/ArApiCompat.csproj @@ -1,8 +1,10 @@  + Exe net9.0 enable + false enable $(NoWarn);CA1043;CA1028 diff --git a/src/build/ArApiCompat/Program.cs b/src/build/ArApiCompat/Program.cs new file mode 100644 index 0000000..cca875c --- /dev/null +++ b/src/build/ArApiCompat/Program.cs @@ -0,0 +1,146 @@ +using ArApiCompat.ApiCompatibility.AssemblyMapping; +using ArApiCompat.ApiCompatibility.Comparing; +using ArApiCompat.ApiCompatibility.Suppressions; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Serialized; +using System.Xml.Linq; + +Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.InvariantCulture; +Thread.CurrentThread.CurrentUICulture = System.Globalization.CultureInfo.InvariantCulture; + +if (args is not [{ } suppressionFile, { } leftAssemblyFile, { } leftRefPathFile, { } rightAssemblyFile, { } rightRefPathFile, ..var rest]) +{ + Console.Error.WriteLine("Usage: ArApiCompat [--write-suppressions]"); + return 1; +} + +var writeSuppression = rest is ["--write-suppressions", ..]; + +var (leftModule, leftUniverse) = LoadModuleInUniverse(leftAssemblyFile, File.ReadAllLines(leftRefPathFile)); +var (rightModule, rightUniverse) = LoadModuleInUniverse(rightAssemblyFile, File.ReadAllLines(rightRefPathFile)); + +var mapping = AssemblyMapper.Create(leftModule.Assembly!, rightModule.Assembly!); +var comparer = new ApiComparer(); +comparer.Compare(mapping); + +var existingSuppressions = new SuppressionFile(); +if (!writeSuppression && File.Exists(suppressionFile)) +{ + existingSuppressions = SuppressionFile.Deserialize(XDocument.Load(suppressionFile)); +} + +var reportedErrorSuppressions = new SuppressionFile(); +// TODO: multiple comparisons at once +var comparison = new SuppressionFile.Comparison() +{ + Left = leftAssemblyFile, + Right = rightAssemblyFile, +}; +reportedErrorSuppressions.Comparisons.Add(comparison); + +var differenceMap = new Dictionary(); +foreach (var diff in comparer.CompatDifferences) +{ + var suppression = new SuppressionFile.Suppression() + { + DifferenceType = diff.Type, + TypeName = diff.GetType().FullName, + Message = diff.Message, + }; + comparison.Suppressions.Add(suppression); + differenceMap.Add(suppression, diff); +} + +reportedErrorSuppressions.Sort(); + +if (!writeSuppression) +{ + // suppress things + var unsuppressed = reportedErrorSuppressions.RemoveSuppressionsFrom(existingSuppressions, out var hasUnused); + + foreach (var c in unsuppressed.Comparisons) + { + foreach (var s in c.Suppressions) + { + Console.WriteLine(differenceMap[s]); + } + } + + if (hasUnused) + { + // TODO: report error + Console.WriteLine("suppressions file had unused suppressions"); + } +} + +if (writeSuppression) +{ + // write the suppressions + var xdoc = reportedErrorSuppressions.Serialize(); + xdoc.Save(suppressionFile, SaveOptions.OmitDuplicateNamespaces); +} + +return 0; + +static (ModuleDefinition module, RuntimeContext universe) LoadModuleInUniverse(string file, string[] referencePath) +{ + var module = (SerializedModuleDefinition)ModuleDefinition.FromFile(file); + var proxyResolver = new ForwardingAssemblyResolver(); + var universe = new RuntimeContext(module.RuntimeContext.TargetRuntime, proxyResolver); + var assemblyResolver = new ReferencePathAsemblyResolver(new() { RuntimeContext = universe }, referencePath); + proxyResolver.Target = assemblyResolver; + module = (SerializedModuleDefinition)ModuleDefinition.FromFile(file, universe.DefaultReaderParameters); + + return (module, universe); +} + +sealed class ReferencePathAsemblyResolver(ModuleReaderParameters mrp, string[] referencePath) : AssemblyResolverBase(mrp) +{ + protected override AssemblyDefinition? ResolveImpl(AssemblyDescriptor assembly) + { + foreach (var file in referencePath) + { + if (Path.GetFileNameWithoutExtension(file).Equals(assembly.Name?.Value.ToUpperInvariant(), StringComparison.OrdinalIgnoreCase)) + { + return LoadAssemblyFromFile(file); + } + } + + return null; + } + + protected override string? ProbeRuntimeDirectories(AssemblyDescriptor assembly) + { + throw new NotImplementedException(); + } +} + +sealed class ForwardingAssemblyResolver : IAssemblyResolver +{ + public IAssemblyResolver? Target { get; set; } + + public void AddToCache(AssemblyDescriptor descriptor, AssemblyDefinition definition) + { + Target?.AddToCache(descriptor, definition); + } + + public void ClearCache() + { + Target?.ClearCache(); + } + + public bool HasCached(AssemblyDescriptor descriptor) + { + return Target?.HasCached(descriptor) ?? false; + } + + public bool RemoveFromCache(AssemblyDescriptor descriptor) + { + return Target?.RemoveFromCache(descriptor) ?? false; + } + + public AssemblyDefinition? Resolve(AssemblyDescriptor assembly) + { + return Target?.Resolve(assembly); + } +} \ No newline at end of file From 636a0d2a80d00df18110a0c110f4d754e2ddbb3b Mon Sep 17 00:00:00 2001 From: DaNike Date: Thu, 6 Nov 2025 03:07:29 -0600 Subject: [PATCH 11/15] Fix generic constraint rule to understand flag constraints and variance --- .../AssemblyMapping/ElementMapper.cs | 80 ++++----- .../ApiCompatibility/Comparing/ApiComparer.cs | 166 +++++++++--------- .../Rules/CannotChangeGenericConstraints.cs | 41 ++++- .../Suppressions/SuppressionFile.cs | 6 - src/build/ArApiCompat/Program.cs | 4 +- 5 files changed, 163 insertions(+), 134 deletions(-) diff --git a/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/ElementMapper.cs b/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/ElementMapper.cs index dd20492..ab0e426 100644 --- a/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/ElementMapper.cs +++ b/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/ElementMapper.cs @@ -1,42 +1,42 @@ -namespace ArApiCompat.ApiCompatibility.AssemblyMapping; - -public abstract class ElementMapper(MapperSettings mapperSettings) +namespace ArApiCompat.ApiCompatibility.AssemblyMapping; + +public abstract class ElementMapper(MapperSettings mapperSettings) { public MapperSettings MapperSettings { get; } = mapperSettings; - - public T? Left { get; set; } - public T? Right { get; set; } - - public T? this[ElementSide side] => side switch - { - ElementSide.Left => Left, - ElementSide.Right => Right, - _ => throw new ArgumentOutOfRangeException(nameof(side), side, null), - }; - - public virtual void Add(T value, ElementSide side) - { - if (this[side] != null) - { - throw new InvalidOperationException($"{side} element already set."); - } - - switch (side) - { - case ElementSide.Left: - Left = value; - break; - case ElementSide.Right: - Right = value; - break; - default: - throw new ArgumentOutOfRangeException(nameof(side), side, null); - } - } - - public void Deconstruct(out T? left, out T? right) - { - left = Left; - right = Right; - } -} + + public T? Left { get; set; } + public T? Right { get; set; } + + public T? this[ElementSide side] => side switch + { + ElementSide.Left => Left, + ElementSide.Right => Right, + _ => throw new ArgumentOutOfRangeException(nameof(side), side, null), + }; + + public virtual void Add(T value, ElementSide side) + { + if (this[side] != null) + { + throw new InvalidOperationException($"{side} element already set."); + } + + switch (side) + { + case ElementSide.Left: + Left = value; + break; + case ElementSide.Right: + Right = value; + break; + default: + throw new ArgumentOutOfRangeException(nameof(side), side, null); + } + } + + public void Deconstruct(out T? left, out T? right) + { + left = Left; + right = Right; + } +} diff --git a/src/build/ArApiCompat/ApiCompatibility/Comparing/ApiComparer.cs b/src/build/ArApiCompat/ApiCompatibility/Comparing/ApiComparer.cs index 805aa7e..3748f1d 100644 --- a/src/build/ArApiCompat/ApiCompatibility/Comparing/ApiComparer.cs +++ b/src/build/ArApiCompat/ApiCompatibility/Comparing/ApiComparer.cs @@ -1,83 +1,83 @@ -using ArApiCompat.ApiCompatibility.AssemblyMapping; -using ArApiCompat.ApiCompatibility.Comparing.Rules; - -namespace ArApiCompat.ApiCompatibility.Comparing; - -public sealed class ApiComparer -{ - private static readonly IEnumerable s_rules = - [ - // new AssemblyIdentityMustMatch(), - new CannotAddAbstractMember(), - new CannotAddMemberToInterface(), - new CannotAddOrRemoveVirtualKeyword(), - new CannotRemoveBaseTypeOrInterface(), - new CannotSealType(), - new EnumsMustMatch(), - new MembersMustExist(), - new CannotChangeVisibility(), - new CannotChangeGenericConstraints(), - ]; - - private readonly List _compatDifferences = []; - - public IEnumerable CompatDifferences => _compatDifferences; - - public void Compare(AssemblyMapper assemblyMapper) - { - ArgumentNullException.ThrowIfNull(assemblyMapper); - - AddDifferences(assemblyMapper); - - foreach (var typeMapper in assemblyMapper.Types) - { - Compare(typeMapper); - } - } - - private void Compare(TypeMapper typeMapper) - { - AddDifferences(typeMapper); - - if (typeMapper.Left == null || typeMapper.Right == null) return; - - foreach (var nestedTypeMapper in typeMapper.NestedTypes) - { - Compare(nestedTypeMapper); - } - - foreach (var memberMapper in typeMapper.Members) - { - Compare(memberMapper); - } - } - - private void Compare(MemberMapper memberMapper) - { - AddDifferences(memberMapper); - } - - private void AddDifferences(AssemblyMapper assemblyMapper) - { - foreach (var rule in s_rules) - { - rule.Run(assemblyMapper, _compatDifferences); - } - } - - private void AddDifferences(TypeMapper typeMapper) - { - foreach (var rule in s_rules) - { - rule.Run(typeMapper, _compatDifferences); - } - } - - private void AddDifferences(MemberMapper memberMapper) - { - foreach (var rule in s_rules) - { - rule.Run(memberMapper, _compatDifferences); - } - } -} +using ArApiCompat.ApiCompatibility.AssemblyMapping; +using ArApiCompat.ApiCompatibility.Comparing.Rules; + +namespace ArApiCompat.ApiCompatibility.Comparing; + +public sealed class ApiComparer +{ + private static readonly IEnumerable s_rules = + [ + // new AssemblyIdentityMustMatch(), + new CannotAddAbstractMember(), + new CannotAddMemberToInterface(), + new CannotAddOrRemoveVirtualKeyword(), + new CannotRemoveBaseTypeOrInterface(), + new CannotSealType(), + new EnumsMustMatch(), + new MembersMustExist(), + new CannotChangeVisibility(), + new CannotChangeGenericConstraints(), + ]; + + private readonly List _compatDifferences = []; + + public IEnumerable CompatDifferences => _compatDifferences; + + public void Compare(AssemblyMapper assemblyMapper) + { + ArgumentNullException.ThrowIfNull(assemblyMapper); + + AddDifferences(assemblyMapper); + + foreach (var typeMapper in assemblyMapper.Types) + { + Compare(typeMapper); + } + } + + private void Compare(TypeMapper typeMapper) + { + AddDifferences(typeMapper); + + if (typeMapper.Left == null || typeMapper.Right == null) return; + + foreach (var nestedTypeMapper in typeMapper.NestedTypes) + { + Compare(nestedTypeMapper); + } + + foreach (var memberMapper in typeMapper.Members) + { + Compare(memberMapper); + } + } + + private void Compare(MemberMapper memberMapper) + { + AddDifferences(memberMapper); + } + + private void AddDifferences(AssemblyMapper assemblyMapper) + { + foreach (var rule in s_rules) + { + rule.Run(assemblyMapper, _compatDifferences); + } + } + + private void AddDifferences(TypeMapper typeMapper) + { + foreach (var rule in s_rules) + { + rule.Run(typeMapper, _compatDifferences); + } + } + + private void AddDifferences(MemberMapper memberMapper) + { + foreach (var rule in s_rules) + { + rule.Run(memberMapper, _compatDifferences); + } + } +} diff --git a/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotChangeGenericConstraints.cs b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotChangeGenericConstraints.cs index c8b9774..85c84b4 100644 --- a/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotChangeGenericConstraints.cs +++ b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotChangeGenericConstraints.cs @@ -1,7 +1,9 @@ -using System.Diagnostics; using ArApiCompat.ApiCompatibility.AssemblyMapping; using ArApiCompat.Utilities.AsmResolver; using AsmResolver.DotNet; +using AsmResolver.PE.DotNet.Metadata.Tables; +using System.Data; +using System.Diagnostics; namespace ArApiCompat.ApiCompatibility.Comparing.Rules; @@ -11,6 +13,13 @@ public sealed class CannotChangeGenericConstraintDifference(DifferenceType type, public override string Message => $"Cannot {(type == DifferenceType.Added ? "add" : "remove")} constraint '{constraint}' on type parameter '{leftTypeParameter}' of '{left}'"; public override DifferenceType Type => type; } + +public sealed class CannotChangeGenericVarianceDifference(DifferenceType type, IMemberDefinition left, GenericParameter leftTypeParameter, string variance, string newVariance) : CompatDifference +{ + public override DifferenceType Type => type; + + public override string Message => $"Cannot change variance of type parameter '{leftTypeParameter}' on '{left}' from {variance} to {newVariance}"; +} #pragma warning restore CS9113 // Parameter is unread. public sealed class CannotChangeGenericConstraints : BaseRule @@ -64,10 +73,14 @@ IList differences var addedConstraints = new List(); var removedConstraints = new List(); - // CompareBoolConstraint(typeParam => typeParam.HasConstructorConstraint, "new()"); TODO - // CompareBoolConstraint(typeParam => typeParam.HasNotNullConstraint, "notnull"); TODO CompareBoolConstraint(typeParam => typeParam.HasReferenceTypeConstraint, "class"); + CompareBoolConstraint(typeParam => typeParam.HasNotNullableValueTypeConstraint, "struct"); + CompareBoolConstraint(typeParam => typeParam.HasDefaultConstructorConstraint, "new()"); + + + // CompareBoolConstraint(typeParam => typeParam.HasNotNullConstraint, "notnull"); TODO // CompareBoolConstraint(typeParam => typeParam.HasUnmanagedTypeConstraint, "unmanaged"); TODO + // unmanaged implies struct // CompareBoolConstraint(typeParam => typeParam.HasValueTypeConstraint & !typeParam.HasUnmanagedTypeConstraint, "struct"); TODO @@ -119,6 +132,19 @@ void CompareBoolConstraint(Func boolConstraint, string c removedConstraints.Add(constraintName); } } + + // while we're here, also check for variance. Variance can ALWAYS be ADDED, but never CHANGED. + // In otherwords, we can make the following changes, and only them: + // Invariant -> Covariant + // Invariant -> Contravariant + var leftVariance = leftTypeParam.Variance; + var rightVariance = rightTypeParam.Variance; + if (leftVariance != rightVariance && leftVariance != GenericParameterAttributes.NonVariant) + { + differences.Add(new CannotChangeGenericVarianceDifference( + rightVariance is GenericParameterAttributes.NonVariant ? DifferenceType.Removed : DifferenceType.Changed, + left, leftTypeParam, FormatVariance(leftVariance), FormatVariance(rightVariance))); + } } } @@ -129,4 +155,13 @@ void CompareBoolConstraint(Func boolConstraint, string c .Where(c => c != null) .ToHashSet(ExtendedSignatureComparer.VersionAgnostic!); } + + private static string FormatVariance(GenericParameterAttributes variance) + => variance switch + { + GenericParameterAttributes.NonVariant => "invariant", + GenericParameterAttributes.Covariant => "covariant ('out')", + GenericParameterAttributes.Contravariant => "contravariant ('in')", + _ => variance.ToString(), + }; } diff --git a/src/build/ArApiCompat/ApiCompatibility/Suppressions/SuppressionFile.cs b/src/build/ArApiCompat/ApiCompatibility/Suppressions/SuppressionFile.cs index f5dd034..ec501d1 100644 --- a/src/build/ArApiCompat/ApiCompatibility/Suppressions/SuppressionFile.cs +++ b/src/build/ArApiCompat/ApiCompatibility/Suppressions/SuppressionFile.cs @@ -1,17 +1,11 @@ using ArApiCompat.ApiCompatibility.Comparing; -using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Xml.Linq; namespace ArApiCompat.ApiCompatibility.Suppressions; internal sealed class SuppressionFile { - public sealed class Comparison { public string? Left { get; set; } diff --git a/src/build/ArApiCompat/Program.cs b/src/build/ArApiCompat/Program.cs index cca875c..daa88e6 100644 --- a/src/build/ArApiCompat/Program.cs +++ b/src/build/ArApiCompat/Program.cs @@ -16,8 +16,8 @@ var writeSuppression = rest is ["--write-suppressions", ..]; -var (leftModule, leftUniverse) = LoadModuleInUniverse(leftAssemblyFile, File.ReadAllLines(leftRefPathFile)); -var (rightModule, rightUniverse) = LoadModuleInUniverse(rightAssemblyFile, File.ReadAllLines(rightRefPathFile)); +var (leftModule, _) = LoadModuleInUniverse(leftAssemblyFile, File.ReadAllLines(leftRefPathFile)); +var (rightModule, _) = LoadModuleInUniverse(rightAssemblyFile, File.ReadAllLines(rightRefPathFile)); var mapping = AssemblyMapper.Create(leftModule.Assembly!, rightModule.Assembly!); var comparer = new ApiComparer(); From 507790e3d88a74694bf203bc94bb5b39fe7ddcc6 Mon Sep 17 00:00:00 2001 From: DaNike Date: Thu, 6 Nov 2025 18:02:14 -0600 Subject: [PATCH 12/15] Implement framework for multi-comparison in ArApiCompat --- .../ApiCompatibility/Comparing/ApiComparer.cs | 2 +- .../Suppressions/SuppressionFile.cs | 3 + src/build/ArApiCompat/ComparisonResult.cs | 255 ++++++++++++++++++ src/build/ArApiCompat/Program.cs | 156 +++-------- 4 files changed, 299 insertions(+), 117 deletions(-) create mode 100644 src/build/ArApiCompat/ComparisonResult.cs diff --git a/src/build/ArApiCompat/ApiCompatibility/Comparing/ApiComparer.cs b/src/build/ArApiCompat/ApiCompatibility/Comparing/ApiComparer.cs index 3748f1d..f7afc68 100644 --- a/src/build/ArApiCompat/ApiCompatibility/Comparing/ApiComparer.cs +++ b/src/build/ArApiCompat/ApiCompatibility/Comparing/ApiComparer.cs @@ -21,7 +21,7 @@ public sealed class ApiComparer private readonly List _compatDifferences = []; - public IEnumerable CompatDifferences => _compatDifferences; + public IReadOnlyList CompatDifferences => _compatDifferences; public void Compare(AssemblyMapper assemblyMapper) { diff --git a/src/build/ArApiCompat/ApiCompatibility/Suppressions/SuppressionFile.cs b/src/build/ArApiCompat/ApiCompatibility/Suppressions/SuppressionFile.cs index ec501d1..007b8d9 100644 --- a/src/build/ArApiCompat/ApiCompatibility/Suppressions/SuppressionFile.cs +++ b/src/build/ArApiCompat/ApiCompatibility/Suppressions/SuppressionFile.cs @@ -12,6 +12,9 @@ public sealed class Comparison public string? Right { get; set; } public List Suppressions { get; } = new(); + + public Suppression? GetSuppressionFor(Suppression suppression) + => Suppressions.FirstOrDefault(s => s == suppression); } public sealed record Suppression diff --git a/src/build/ArApiCompat/ComparisonResult.cs b/src/build/ArApiCompat/ComparisonResult.cs new file mode 100644 index 0000000..0cf7176 --- /dev/null +++ b/src/build/ArApiCompat/ComparisonResult.cs @@ -0,0 +1,255 @@ +using ArApiCompat.ApiCompatibility.AssemblyMapping; +using ArApiCompat.ApiCompatibility.Comparing; +using ArApiCompat.ApiCompatibility.Suppressions; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Serialized; + +namespace ArApiCompat; + +internal sealed record ComparisonJob( + string LeftName, + string LeftAssembly, + IReadOnlyList LeftReferencePath, + string RightName, + string RightAssembly, + IReadOnlyList RightReferencePath + ); + +internal sealed class ComparisonResult +{ + public static ComparisonResult Execute(IEnumerable jobs, SuppressionFile? suppressions, ParallelOptions? parallelization = null) + { + var allJobs = jobs.ToArray(); + + allJobs.Sort((a, b) + => StringComparer.Ordinal.Compare(a.LeftName, b.LeftName) * 2 + + StringComparer.Ordinal.Compare(a.RightName, b.RightName)); + + var allComparers = new ApiComparer[allJobs.Length]; + + Parallel.For(0, allJobs.Length, parallelization ?? new(), i => + { + var job = allJobs[i]; + + var (left, _) = LoadModuleInNewUniverse(job.LeftAssembly, job.LeftReferencePath); + var (right, _) = LoadModuleInNewUniverse(job.RightAssembly, job.RightReferencePath); + + var mapper = AssemblyMapper.Create(left.Assembly!, right.Assembly!); + + var comparer = new ApiComparer(); + comparer.Compare(mapper); + allComparers[i] = comparer; + }); + + return new(allJobs, allComparers, suppressions); + } + + private static (ModuleDefinition module, RuntimeContext universe) LoadModuleInNewUniverse(string file, IReadOnlyList referencePath) + { + var module = (SerializedModuleDefinition)ModuleDefinition.FromFile(file); + var proxyResolver = new ForwardingAssemblyResolver(); + var universe = new RuntimeContext(module.RuntimeContext.TargetRuntime, proxyResolver); + var assemblyResolver = new ReferencePathAsemblyResolver(new() { RuntimeContext = universe }, referencePath); + proxyResolver.Target = assemblyResolver; + module = (SerializedModuleDefinition)ModuleDefinition.FromFile(file, universe.DefaultReaderParameters); + + return (module, universe); + } + + private sealed class ReferencePathAsemblyResolver(ModuleReaderParameters mrp, IReadOnlyList referencePath) : AssemblyResolverBase(mrp) + { + protected override AssemblyDefinition? ResolveImpl(AssemblyDescriptor assembly) + { + foreach (var file in referencePath) + { + if (Path.GetFileNameWithoutExtension(file).Equals(assembly.Name?.Value.ToUpperInvariant(), StringComparison.OrdinalIgnoreCase)) + { + return LoadAssemblyFromFile(file); + } + } + + return null; + } + + protected override string? ProbeRuntimeDirectories(AssemblyDescriptor assembly) + { + throw new NotImplementedException(); + } + } + + private sealed class ForwardingAssemblyResolver : IAssemblyResolver + { + public ReferencePathAsemblyResolver? Target { get; set; } + + public void AddToCache(AssemblyDescriptor descriptor, AssemblyDefinition definition) + { + Target?.AddToCache(descriptor, definition); + } + + public void ClearCache() + { + Target?.ClearCache(); + } + + public bool HasCached(AssemblyDescriptor descriptor) + { + return Target?.HasCached(descriptor) ?? false; + } + + public bool RemoveFromCache(AssemblyDescriptor descriptor) + { + return Target?.RemoveFromCache(descriptor) ?? false; + } + + public AssemblyDefinition? Resolve(AssemblyDescriptor assembly) + { + return Target?.Resolve(assembly); + } + } + + private ComparisonResult( + ComparisonJob[] jobs, + ApiComparer[] comparers, + SuppressionFile? suppressions) + { + this.jobs = jobs; + this.comparers = comparers; + this.suppressions = suppressions; + suppressedDifferences = new IReadOnlyList[jobs.Length]; + + // initialize suppressionsHasUnused with whether there are comparisons with suppressions that we don't have + if (suppressions is not null) + { + suppressionsHasUnused = !suppressions.Comparisons + .All(c => jobs.Any(j => j.LeftName == c.Left && j.RightName == c.Right)); + } + } + + private readonly ComparisonJob[] jobs; + private readonly ApiComparer[] comparers; + private readonly SuppressionFile? suppressions; + private readonly IReadOnlyList?[] suppressedDifferences; + private bool suppressionsHasUnused; + + public int JobCount => jobs.Length; + public IReadOnlyList Jobs => jobs; + public IReadOnlyList GetRawDifferences(int i) + => comparers[i].CompatDifferences; + + public IReadOnlyList GetDifferences(int i) + { + var list = suppressedDifferences[i]; + if (list is null) + { + _ = Interlocked.CompareExchange(ref suppressedDifferences[i], + ComputeSuppresedDifferences(i), + null); + list = suppressedDifferences[i]!; + } + return list; + } + + private IReadOnlyList ComputeSuppresedDifferences(int i) + { + var job = jobs[i]; + var comparer = comparers[i]; + var suppressionJob = suppressions?.GetComparison(job.LeftName, job.RightName); + if (suppressionJob is null || suppressionJob.Suppressions.Count == 0) + { + // no suppressions, just return the raw compat difference list + return comparer.CompatDifferences; + } + + // we have suppressions, do something about it + var usedSuppressions = new HashSet(ReferenceEqualityComparer.Instance); + var result = new List(comparer.CompatDifferences.Count); + + foreach (var difference in comparer.CompatDifferences) + { + var suppression = suppressionJob.Suppressions + .FirstOrDefault(s + => s.DifferenceType == difference.Type + && s.TypeName == difference.GetType().FullName + && s.Message == difference.Message); + + if (suppression is not null) + { + // difference is suppressed, record that we used the suppression + _ = usedSuppressions.Add(suppression); + } + else + { + // difference is not suppressed, record the difference + result.Add(difference); + } + } + + // we've gone through all of the suppressions, check if any were unused + if (!suppressionJob.Suppressions.All(usedSuppressions.Contains)) + { + // we didn't end up using some suppressions, note that down + suppressionsHasUnused = true; + } + + return result; + } + + public bool HasUnusedSuppressions + { + get + { + if (suppressionsHasUnused) return true; + + // if suppressionsHasUnused is false, we need to make sure we've computed the suppressed differences for everything + for (var i = 0; i < JobCount; i++) + { + _ = GetDifferences(i); + } + + // once that's done, that's our result + return suppressionsHasUnused; + } + } + + public SuppressionFile GetSuppressionFile() + { + var result = new SuppressionFile(); + + for (var i = 0; i < JobCount; i++) + { + var job = jobs[i]; + var comparer = comparers[i]; + + var comparison = new SuppressionFile.Comparison() + { + Left = job.LeftName, + Right = job.RightName, + }; + + foreach (var diff in comparer.CompatDifferences) + { + comparison.Suppressions.Add(FormatSuppression(diff)); + } + + if (comparison.Suppressions.Count > 0) + { + result.Comparisons.Add(comparison); + } + } + + result.Sort(); // need this despite keeping jobs sorted from the get-go because we want to sort compat differences too + + return result; + } + + private static SuppressionFile.Suppression FormatSuppression(CompatDifference difference) + { + return new() + { + DifferenceType = difference.Type, + TypeName = difference.GetType().FullName, + Message = difference.Message + }; + } + +} diff --git a/src/build/ArApiCompat/Program.cs b/src/build/ArApiCompat/Program.cs index daa88e6..4cb9e58 100644 --- a/src/build/ArApiCompat/Program.cs +++ b/src/build/ArApiCompat/Program.cs @@ -1,8 +1,5 @@ -using ArApiCompat.ApiCompatibility.AssemblyMapping; -using ArApiCompat.ApiCompatibility.Comparing; +using ArApiCompat; using ArApiCompat.ApiCompatibility.Suppressions; -using AsmResolver.DotNet; -using AsmResolver.DotNet.Serialized; using System.Xml.Linq; Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.InvariantCulture; @@ -16,131 +13,58 @@ var writeSuppression = rest is ["--write-suppressions", ..]; -var (leftModule, _) = LoadModuleInUniverse(leftAssemblyFile, File.ReadAllLines(leftRefPathFile)); -var (rightModule, _) = LoadModuleInUniverse(rightAssemblyFile, File.ReadAllLines(rightRefPathFile)); - -var mapping = AssemblyMapper.Create(leftModule.Assembly!, rightModule.Assembly!); -var comparer = new ApiComparer(); -comparer.Compare(mapping); - -var existingSuppressions = new SuppressionFile(); -if (!writeSuppression && File.Exists(suppressionFile)) -{ - existingSuppressions = SuppressionFile.Deserialize(XDocument.Load(suppressionFile)); -} - -var reportedErrorSuppressions = new SuppressionFile(); -// TODO: multiple comparisons at once -var comparison = new SuppressionFile.Comparison() -{ - Left = leftAssemblyFile, - Right = rightAssemblyFile, -}; -reportedErrorSuppressions.Comparisons.Add(comparison); - -var differenceMap = new Dictionary(); -foreach (var diff in comparer.CompatDifferences) -{ - var suppression = new SuppressionFile.Suppression() - { - DifferenceType = diff.Type, - TypeName = diff.GetType().FullName, - Message = diff.Message, - }; - comparison.Suppressions.Add(suppression); - differenceMap.Add(suppression, diff); -} - -reportedErrorSuppressions.Sort(); - -if (!writeSuppression) -{ - // suppress things - var unsuppressed = reportedErrorSuppressions.RemoveSuppressionsFrom(existingSuppressions, out var hasUnused); - - foreach (var c in unsuppressed.Comparisons) - { - foreach (var s in c.Suppressions) - { - Console.WriteLine(differenceMap[s]); - } - } - - if (hasUnused) - { - // TODO: report error - Console.WriteLine("suppressions file had unused suppressions"); - } -} +var result = ComparisonResult.Execute( + [ + new( + Path.GetFileName(Path.GetDirectoryName(leftAssemblyFile)) ?? leftAssemblyFile, + leftAssemblyFile, + File.ReadAllLines(leftRefPathFile), + Path.GetFileName(Path.GetDirectoryName(rightAssemblyFile)) ?? leftAssemblyFile, + rightAssemblyFile, + File.ReadAllLines(rightRefPathFile) + ) + ], + File.Exists(suppressionFile) + ? SuppressionFile.Deserialize(XDocument.Load(suppressionFile)) + : null); if (writeSuppression) { - // write the suppressions - var xdoc = reportedErrorSuppressions.Serialize(); - xdoc.Save(suppressionFile, SaveOptions.OmitDuplicateNamespaces); -} - -return 0; - -static (ModuleDefinition module, RuntimeContext universe) LoadModuleInUniverse(string file, string[] referencePath) -{ - var module = (SerializedModuleDefinition)ModuleDefinition.FromFile(file); - var proxyResolver = new ForwardingAssemblyResolver(); - var universe = new RuntimeContext(module.RuntimeContext.TargetRuntime, proxyResolver); - var assemblyResolver = new ReferencePathAsemblyResolver(new() { RuntimeContext = universe }, referencePath); - proxyResolver.Target = assemblyResolver; - module = (SerializedModuleDefinition)ModuleDefinition.FromFile(file, universe.DefaultReaderParameters); + var suppressions = result.GetSuppressionFile(); + var doc = suppressions.Serialize(); + doc.Save(suppressionFile, SaveOptions.OmitDuplicateNamespaces); - return (module, universe); + return 0; } - -sealed class ReferencePathAsemblyResolver(ModuleReaderParameters mrp, string[] referencePath) : AssemblyResolverBase(mrp) +else { - protected override AssemblyDefinition? ResolveImpl(AssemblyDescriptor assembly) + var anyError = false; + for (var i = 0; i < result.JobCount; i++) { - foreach (var file in referencePath) + var job = result.Jobs[i]; + var differences = result.GetDifferences(i); + if (differences.Count > 0) { - if (Path.GetFileNameWithoutExtension(file).Equals(assembly.Name?.Value.ToUpperInvariant(), StringComparison.OrdinalIgnoreCase)) + anyError = true; + Console.WriteLine($"error : Compatability errors between '{job.LeftName}' and '{job.RightName}':"); + + foreach (var difference in differences) { - return LoadAssemblyFromFile(file); + Console.WriteLine($"error {difference}"); } - } - return null; - } - - protected override string? ProbeRuntimeDirectories(AssemblyDescriptor assembly) - { - throw new NotImplementedException(); - } -} - -sealed class ForwardingAssemblyResolver : IAssemblyResolver -{ - public IAssemblyResolver? Target { get; set; } - - public void AddToCache(AssemblyDescriptor descriptor, AssemblyDefinition definition) - { - Target?.AddToCache(descriptor, definition); - } - - public void ClearCache() - { - Target?.ClearCache(); - } - - public bool HasCached(AssemblyDescriptor descriptor) - { - return Target?.HasCached(descriptor) ?? false; + Console.WriteLine("---"); + } + else + { + // no differences, don't report anything + } } - public bool RemoveFromCache(AssemblyDescriptor descriptor) + if (result.HasUnusedSuppressions) { - return Target?.RemoveFromCache(descriptor) ?? false; + Console.WriteLine($"warning : Suppressions file '{suppressionFile}' has unused suppressions. Regenerate it by passing --write-suppressions to ArApiCompat."); } - public AssemblyDefinition? Resolve(AssemblyDescriptor assembly) - { - return Target?.Resolve(assembly); - } -} \ No newline at end of file + return anyError ? 1 : 0; +} From 0a7322e5a09c4b23b7f0f8c793a76c3fe517f210 Mon Sep 17 00:00:00 2001 From: DaNike Date: Fri, 7 Nov 2025 00:25:28 -0600 Subject: [PATCH 13/15] Implement reading from single job description file --- src/build/ArApiCompat/Program.cs | 113 +++++++++++++++++++++++++++---- 1 file changed, 101 insertions(+), 12 deletions(-) diff --git a/src/build/ArApiCompat/Program.cs b/src/build/ArApiCompat/Program.cs index 4cb9e58..e70796d 100644 --- a/src/build/ArApiCompat/Program.cs +++ b/src/build/ArApiCompat/Program.cs @@ -5,25 +5,114 @@ Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.InvariantCulture; Thread.CurrentThread.CurrentUICulture = System.Globalization.CultureInfo.InvariantCulture; -if (args is not [{ } suppressionFile, { } leftAssemblyFile, { } leftRefPathFile, { } rightAssemblyFile, { } rightRefPathFile, ..var rest]) +if (args is not [{ } suppressionFile, { } comparisonsDef, ..var rest]) { - Console.Error.WriteLine("Usage: ArApiCompat [--write-suppressions]"); + Console.Error.WriteLine("Usage: ArApiCompat [--write-suppressions]"); return 1; } var writeSuppression = rest is ["--write-suppressions", ..]; +var comparisonJobs = new List(); +var comparisonDefsFile = File.ReadAllLines(comparisonsDef); + +var idx = 0; +var lineCount = comparisonDefsFile.Length; + +(string? line, int lineNumber) ReadNonEmptyLine() +{ + while (idx < lineCount) + { + var currentIndex = idx; + var l = comparisonDefsFile[idx++].Trim(); + if (l.Length == 0) continue; + // line numbers are 1-based + return (l, currentIndex + 1); + } + return (null, lineCount > 0 ? lineCount : 1); +} + +while (true) +{ + var (leftName, leftNameLine) = ReadNonEmptyLine(); + if (leftName is null) break; // done + + var (leftFile, _) = ReadNonEmptyLine(); + if (leftFile is null) + { + Console.Error.WriteLine($"{comparisonsDef}({leftNameLine}): error ARAPI0001: Missing left file for comparison starting with '{leftName}'"); + return 1; + } + + var (leftRefCountLine, leftRefCountLineNum) = ReadNonEmptyLine(); + if (leftRefCountLine is null || !int.TryParse(leftRefCountLine, out var leftRefCount) || leftRefCount < 0) + { + Console.Error.WriteLine($"{comparisonsDef}({leftRefCountLineNum}): error ARAPI0002: Missing or invalid left reference count for comparison starting with '{leftName}'"); + return 1; + } + + var leftRefPath = new List(leftRefCount); + for (var i = 0; i < leftRefCount; i++) + { + var (p, _) = ReadNonEmptyLine(); + if (p is null) + { + Console.Error.WriteLine($"{comparisonsDef}({lineCount}): error ARAPI0003: Not enough left reference paths for comparison starting with '{leftName}'"); + return 1; + } + leftRefPath.Add(p); + } + + var (rightName, rightNameLine) = ReadNonEmptyLine(); + if (rightName is null) + { + Console.Error.WriteLine($"{comparisonsDef}({leftNameLine}): error ARAPI0004: Missing right name for comparison starting with '{leftName}'"); + return 1; + } + + var (rightFile, _) = ReadNonEmptyLine(); + if (rightFile is null) + { + Console.Error.WriteLine($"{comparisonsDef}({rightNameLine}): error ARAPI0005: Missing right file for comparison starting with '{leftName}'"); + return 1; + } + + var (rightRefCountLine, rightRefCountLineNum) = ReadNonEmptyLine(); + if (rightRefCountLine is null || !int.TryParse(rightRefCountLine, out var rightRefCount) || rightRefCount < 0) + { + Console.Error.WriteLine($"{comparisonsDef}({rightRefCountLineNum}): error ARAPI0006: Missing or invalid right reference count for comparison starting with '{leftName}'"); + return 1; + } + + var rightRefPath = new List(rightRefCount); + for (var i = 0; i < rightRefCount; i++) + { + var (p, _) = ReadNonEmptyLine(); + if (p is null) + { + Console.Error.WriteLine($"{comparisonsDef}({lineCount}): error ARAPI0007: Not enough right reference paths for comparison starting with '{leftName}'"); + return 1; + } + rightRefPath.Add(p); + } + + comparisonJobs.Add(new( + leftName, + leftFile, + leftRefPath.ToArray(), + rightName, + rightFile, + rightRefPath.ToArray())); +} + +if (comparisonJobs.Count == 0) +{ + Console.Error.WriteLine($"{comparisonsDef}(1): error ARAPI0008: No comparisons found in definitions file."); + return 1; +} + var result = ComparisonResult.Execute( - [ - new( - Path.GetFileName(Path.GetDirectoryName(leftAssemblyFile)) ?? leftAssemblyFile, - leftAssemblyFile, - File.ReadAllLines(leftRefPathFile), - Path.GetFileName(Path.GetDirectoryName(rightAssemblyFile)) ?? leftAssemblyFile, - rightAssemblyFile, - File.ReadAllLines(rightRefPathFile) - ) - ], + comparisonJobs, File.Exists(suppressionFile) ? SuppressionFile.Deserialize(XDocument.Load(suppressionFile)) : null); From c5acf25c8f7639d456afdd15aea1fac2c4df21e0 Mon Sep 17 00:00:00 2001 From: DaNike Date: Fri, 7 Nov 2025 18:57:47 -0600 Subject: [PATCH 14/15] Make Backports.Shims use new ArApiCompat --- src/MonoMod.Backports.Shims/ApiCompat.targets | 96 ++++++++----- .../CompatibilitySuppressions.xml | 133 +++++++++++------- src/build/ArApiCompat/ArApiCompat.csproj | 2 +- src/build/ArApiCompat/Program.cs | 41 ++++-- 4 files changed, 172 insertions(+), 100 deletions(-) diff --git a/src/MonoMod.Backports.Shims/ApiCompat.targets b/src/MonoMod.Backports.Shims/ApiCompat.targets index a2af8ed..5fd044d 100644 --- a/src/MonoMod.Backports.Shims/ApiCompat.targets +++ b/src/MonoMod.Backports.Shims/ApiCompat.targets @@ -15,6 +15,12 @@ Private="false" Pack="false" SetTargetFramework="TargetFramework=net9.0" SkipGetTargetFrameworkProperties="true" /> + @@ -192,60 +198,76 @@ - + + + <_ArCompareDef Remove="@(_ArCompareDef)" /> + + + + <_Tfm>%(_GenApiCompatParsed.Tfm) + <_Dll>%(_GenApiCompatParsed.Dll) + <_LeftRefPath>%(_GenApiCompatParsed.LeftRefPath) + <_RightRefPath>%(_GenApiCompatParsed.RightRefPath) <_RefPath> <_RefPath Condition="'$(_Tfm)' == '%(_ApiCompatRefPath.Identity)'">%(_ApiCompatRefPath.ReferencePath) + - <_GenApiCompatParsed Update="%(_GenApiCompatParsed.Identity)"> - %(LeftRefPath),$(_RefPath) - %(RightRefPath),$(_RefPath) - + <_LRefPath Remove="@(_LRefPath)" /> + <_LRefPath Include="$([System.String]::Copy('$(_LeftRefPath)').Split(','))" /> + <_LRefPath Include="$([System.String]::Copy('$(_RefPath)').Split(','))" /> + + <_RRefPath Remove="@(_RRefPath)" /> + <_RRefPath Include="$([System.String]::Copy('$(_RightRefPath)').Split(','))" /> + <_RRefPath Include="$([System.String]::Copy('$(_RefPath)').Split(','))" /> - - - - + <_ArCompareDef Include="$(_Tfm) baseline" /> + <_ArCompareDef Include="$(_Dll)" /> + <_ArCompareDef Include="@(_LRefPath->Count())" /> + <_ArCompareDef Include="@(_LRefPath)"/> + + <_ArCompareDef Include="$(_Tfm) shimmed" /> + <_ArCompareDef Include="$(_Dll)" /> + <_ArCompareDef Include="@(_RRefPath->Count())" /> + <_ArCompareDef Include="@(_RRefPath)"/> - <_ApiCompatValidateAssembliesSemaphoreFile>$(IntermediateOutputPath)$(MSBuildThisFileName).apicompat.semaphore - CollectApiCompatInputs;$(ApiCompatValidateAssembliesDependsOn);_ApiCompatFinalizeInputs;_WaitForBackportsBuild + $(IntermediateOutputPath)comparisons.txt - + DependsOnTargets="_ApiCompatSelectRefPath;CollectApiCompatInputs"> + + + + + <_ArApiCompatExe>%(MMArApiCompat.RelativeDir)%(FileName)$(_NativeExecutableExtension) + + + + <_PPArguments Remove="@(_PPArguments)" /> + + <_PPArguments Include="@(ApiCompatSuppressionFile)" /> + + <_PPArguments Include="$(ArApiCompatComparisonDefsFile)" /> + + <_PPArguments Include="--write-suppressions" Condition="'$(ApiCompatGenerateSuppressionFile)' == 'true'" /> + <_PPArguments Include="Pass '-p:ApiCompatGenerateSuppressionsFile=true' to regenerate." Condition="'$(ApiCompatGenerateSuppressionFile)' != 'true'" /> + + + + + + + \ No newline at end of file diff --git a/src/MonoMod.Backports.Shims/CompatibilitySuppressions.xml b/src/MonoMod.Backports.Shims/CompatibilitySuppressions.xml index bcc2e08..b074b51 100644 --- a/src/MonoMod.Backports.Shims/CompatibilitySuppressions.xml +++ b/src/MonoMod.Backports.Shims/CompatibilitySuppressions.xml @@ -1,52 +1,83 @@  - - - - CP0021 - M:System.Runtime.CompilerServices.Unsafe.Unbox``1(System.Object)``0:struct - net45 baseline - net45 shimmed - - - CP0021 - M:System.Runtime.CompilerServices.Unsafe.Unbox``1(System.Object)``0:struct - net452 baseline - net452 shimmed - - - CP0021 - M:System.Runtime.CompilerServices.Unsafe.Unbox``1(System.Object)``0:struct - net461 baseline - net461 shimmed - - - CP0021 - M:System.Runtime.CompilerServices.Unsafe.Unbox``1(System.Object)``0:struct - net5.0 baseline - net5.0 shimmed - - - CP0021 - M:System.Runtime.CompilerServices.Unsafe.Unbox``1(System.Object)``0:struct - netcoreapp2.0 baseline - netcoreapp2.0 shimmed - - - CP0021 - M:System.Runtime.CompilerServices.Unsafe.Unbox``1(System.Object)``0:struct - netcoreapp2.1 baseline - netcoreapp2.1 shimmed - - - CP0021 - M:System.Runtime.CompilerServices.Unsafe.Unbox``1(System.Object)``0:struct - netcoreapp3.0 baseline - netcoreapp3.0 shimmed - - - CP0021 - M:System.Runtime.CompilerServices.Unsafe.Unbox``1(System.Object)``0:struct - netcoreapp3.1 baseline - netcoreapp3.1 shimmed - - \ No newline at end of file + + + + ArApiCompat.ApiCompatibility.Comparing.Rules.CannotChangeGenericConstraintDifference + Cannot add constraint 'new()' on type parameter 'T' of '!!0& System.Runtime.CompilerServices.Unsafe::Unbox<T>(System.Object)' + + + ArApiCompat.ApiCompatibility.Comparing.Rules.CannotChangeGenericConstraintDifference + Cannot add constraint 'struct' on type parameter 'T' of '!!0& System.Runtime.CompilerServices.Unsafe::Unbox<T>(System.Object)' + + + + + ArApiCompat.ApiCompatibility.Comparing.Rules.CannotChangeGenericConstraintDifference + Cannot add constraint 'new()' on type parameter 'T' of '!!0& System.Runtime.CompilerServices.Unsafe::Unbox<T>(System.Object)' + + + ArApiCompat.ApiCompatibility.Comparing.Rules.CannotChangeGenericConstraintDifference + Cannot add constraint 'struct' on type parameter 'T' of '!!0& System.Runtime.CompilerServices.Unsafe::Unbox<T>(System.Object)' + + + + + ArApiCompat.ApiCompatibility.Comparing.Rules.CannotChangeGenericConstraintDifference + Cannot add constraint 'new()' on type parameter 'T' of '!!0& System.Runtime.CompilerServices.Unsafe::Unbox<T>(System.Object)' + + + ArApiCompat.ApiCompatibility.Comparing.Rules.CannotChangeGenericConstraintDifference + Cannot add constraint 'struct' on type parameter 'T' of '!!0& System.Runtime.CompilerServices.Unsafe::Unbox<T>(System.Object)' + + + + + ArApiCompat.ApiCompatibility.Comparing.Rules.CannotChangeGenericConstraintDifference + Cannot add constraint 'new()' on type parameter 'T' of '!!0& System.Runtime.CompilerServices.Unsafe::Unbox<T>(System.Object)' + + + ArApiCompat.ApiCompatibility.Comparing.Rules.CannotChangeGenericConstraintDifference + Cannot add constraint 'struct' on type parameter 'T' of '!!0& System.Runtime.CompilerServices.Unsafe::Unbox<T>(System.Object)' + + + + + ArApiCompat.ApiCompatibility.Comparing.Rules.CannotChangeGenericConstraintDifference + Cannot add constraint 'new()' on type parameter 'T' of '!!0& System.Runtime.CompilerServices.Unsafe::Unbox<T>(System.Object)' + + + ArApiCompat.ApiCompatibility.Comparing.Rules.CannotChangeGenericConstraintDifference + Cannot add constraint 'struct' on type parameter 'T' of '!!0& System.Runtime.CompilerServices.Unsafe::Unbox<T>(System.Object)' + + + + + ArApiCompat.ApiCompatibility.Comparing.Rules.CannotChangeGenericConstraintDifference + Cannot add constraint 'new()' on type parameter 'T' of '!!0& System.Runtime.CompilerServices.Unsafe::Unbox<T>(System.Object)' + + + ArApiCompat.ApiCompatibility.Comparing.Rules.CannotChangeGenericConstraintDifference + Cannot add constraint 'struct' on type parameter 'T' of '!!0& System.Runtime.CompilerServices.Unsafe::Unbox<T>(System.Object)' + + + + + ArApiCompat.ApiCompatibility.Comparing.Rules.CannotChangeGenericConstraintDifference + Cannot add constraint 'new()' on type parameter 'T' of '!!0& System.Runtime.CompilerServices.Unsafe::Unbox<T>(System.Object)' + + + ArApiCompat.ApiCompatibility.Comparing.Rules.CannotChangeGenericConstraintDifference + Cannot add constraint 'struct' on type parameter 'T' of '!!0& System.Runtime.CompilerServices.Unsafe::Unbox<T>(System.Object)' + + + + + ArApiCompat.ApiCompatibility.Comparing.Rules.CannotChangeGenericConstraintDifference + Cannot add constraint 'new()' on type parameter 'T' of '!!0& System.Runtime.CompilerServices.Unsafe::Unbox<T>(System.Object)' + + + ArApiCompat.ApiCompatibility.Comparing.Rules.CannotChangeGenericConstraintDifference + Cannot add constraint 'struct' on type parameter 'T' of '!!0& System.Runtime.CompilerServices.Unsafe::Unbox<T>(System.Object)' + + + \ No newline at end of file diff --git a/src/build/ArApiCompat/ArApiCompat.csproj b/src/build/ArApiCompat/ArApiCompat.csproj index 09708e0..6348df6 100644 --- a/src/build/ArApiCompat/ArApiCompat.csproj +++ b/src/build/ArApiCompat/ArApiCompat.csproj @@ -6,7 +6,7 @@ enable false enable - $(NoWarn);CA1043;CA1028 + $(NoWarn);CA1043;CA1028;CA1031 diff --git a/src/build/ArApiCompat/Program.cs b/src/build/ArApiCompat/Program.cs index e70796d..f68a79f 100644 --- a/src/build/ArApiCompat/Program.cs +++ b/src/build/ArApiCompat/Program.cs @@ -7,11 +7,16 @@ if (args is not [{ } suppressionFile, { } comparisonsDef, ..var rest]) { - Console.Error.WriteLine("Usage: ArApiCompat [--write-suppressions]"); + Console.Error.WriteLine("Usage: ArApiCompat [--write-suppressions|]"); return 1; } var writeSuppression = rest is ["--write-suppressions", ..]; +var genSuppressionsMessage = "Regenerate it by passing --write-suppressions to ArApiCompat."; +if (!writeSuppression && rest is [{ } message, ..]) +{ + genSuppressionsMessage = message; +} var comparisonJobs = new List(); var comparisonDefsFile = File.ReadAllLines(comparisonsDef); @@ -111,23 +116,36 @@ return 1; } +var anyError = false; + +SuppressionFile? suppressionFileModel = null; +try +{ + suppressionFileModel = File.Exists(suppressionFile) + ? SuppressionFile.Deserialize(XDocument.Load(suppressionFile)) + : null; +} +catch (Exception e) +{ + if (!writeSuppression) + { + Console.WriteLine($"{suppressionFile}(1): error : Invalid suppression file: {e}"); + Console.WriteLine($"warning : {genSuppressionsMessage}"); + anyError = true; + } +} + var result = ComparisonResult.Execute( - comparisonJobs, - File.Exists(suppressionFile) - ? SuppressionFile.Deserialize(XDocument.Load(suppressionFile)) - : null); + comparisonJobs, suppressionFileModel); if (writeSuppression) { var suppressions = result.GetSuppressionFile(); var doc = suppressions.Serialize(); doc.Save(suppressionFile, SaveOptions.OmitDuplicateNamespaces); - - return 0; } else { - var anyError = false; for (var i = 0; i < result.JobCount; i++) { var job = result.Jobs[i]; @@ -152,8 +170,9 @@ if (result.HasUnusedSuppressions) { - Console.WriteLine($"warning : Suppressions file '{suppressionFile}' has unused suppressions. Regenerate it by passing --write-suppressions to ArApiCompat."); + Console.WriteLine($"warning : Suppressions file '{suppressionFile}' has unused suppressions. {genSuppressionsMessage}"); } - - return anyError ? 1 : 0; } + + +return anyError ? 1 : 0; \ No newline at end of file From 7e885963105164a95edecc80a93f5099d48eb325 Mon Sep 17 00:00:00 2001 From: DaNike Date: Thu, 6 Nov 2025 03:07:29 -0600 Subject: [PATCH 15/15] Fix general member equality in ApiCompat mappers --- src/build/ArApiCompat/ArApiCompat.csproj | 2 +- .../AsmResolver/ExtendedSignatureComparer.cs | 210 ++++++++++-------- 2 files changed, 120 insertions(+), 92 deletions(-) diff --git a/src/build/ArApiCompat/ArApiCompat.csproj b/src/build/ArApiCompat/ArApiCompat.csproj index 6348df6..b38574a 100644 --- a/src/build/ArApiCompat/ArApiCompat.csproj +++ b/src/build/ArApiCompat/ArApiCompat.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/build/ArApiCompat/Utilities/AsmResolver/ExtendedSignatureComparer.cs b/src/build/ArApiCompat/Utilities/AsmResolver/ExtendedSignatureComparer.cs index 6c9ba7e..aad62d9 100644 --- a/src/build/ArApiCompat/Utilities/AsmResolver/ExtendedSignatureComparer.cs +++ b/src/build/ArApiCompat/Utilities/AsmResolver/ExtendedSignatureComparer.cs @@ -1,91 +1,119 @@ -using System.Diagnostics.CodeAnalysis; -using AsmResolver.DotNet; -using AsmResolver.DotNet.Signatures; - -namespace ArApiCompat.Utilities.AsmResolver; - -// TODO upstream? - -/// -/// A with added support for , and . -/// -[SuppressMessage("Design", "CA1061:Do not hide base class methods", Justification = "Hidden base class methods eventually get called regardless.")] -internal sealed class ExtendedSignatureComparer : SignatureComparer, - IEqualityComparer, - IEqualityComparer, - IEqualityComparer -{ - public ExtendedSignatureComparer() - { - } - - public ExtendedSignatureComparer(SignatureComparisonFlags flags) : base(flags) - { - } - - public static new ExtendedSignatureComparer Default { get; } = new(); - public static ExtendedSignatureComparer VersionAgnostic { get; } = new(SignatureComparisonFlags.VersionAgnostic); - - public bool Equals(IMemberDescriptor? x, IMemberDescriptor? y) - { - if (ReferenceEquals(x, y)) return true; - if (x == null || y == null) return false; - - return x switch - { - ITypeDescriptor type => base.Equals(type, y as ITypeDescriptor), - IMethodDescriptor method => base.Equals(method, y as IMethodDescriptor), - IFieldDescriptor field => base.Equals(field, y as IFieldDescriptor), - PropertyDefinition property => Equals(property, y as PropertyDefinition), - EventDefinition @event => Equals(@event, y as EventDefinition), - _ => false, - }; - } - - public int GetHashCode(IMemberDescriptor obj) - { - return obj switch - { - ITypeDescriptor type => base.GetHashCode(type), - IMethodDescriptor method => base.GetHashCode(method), - IFieldDescriptor field => base.GetHashCode(field), - PropertyDefinition property => GetHashCode(property), - EventDefinition @event => GetHashCode(@event), - _ => throw new ArgumentOutOfRangeException(nameof(obj)), - }; - } - - public bool Equals(PropertyDefinition? x, PropertyDefinition? y) - { - if (ReferenceEquals(x, y)) return true; - if (x == null || y == null) return false; - - return x.Name == y.Name && base.Equals(x.DeclaringType, y.DeclaringType); - } - - public int GetHashCode(PropertyDefinition obj) - { - return HashCode.Combine( - obj.Name, - obj.DeclaringType == null ? 0 : base.GetHashCode(obj.DeclaringType), - obj.Signature == null ? 0 : base.GetHashCode(obj.Signature) - ); - } - - public bool Equals(EventDefinition? x, EventDefinition? y) - { - if (ReferenceEquals(x, y)) return true; - if (x == null || y == null) return false; - - return x.Name == y.Name && base.Equals(x.DeclaringType, y.DeclaringType); - } - - public int GetHashCode(EventDefinition obj) - { - return HashCode.Combine( - obj.Name, - obj.DeclaringType == null ? 0 : base.GetHashCode(obj.DeclaringType), - obj.EventType == null ? 0 : base.GetHashCode(obj.EventType) - ); - } -} +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using System.Diagnostics.CodeAnalysis; + +namespace ArApiCompat.Utilities.AsmResolver; + +// TODO upstream? + +/// +/// A with added support for , and . +/// +[SuppressMessage("Design", "CA1061:Do not hide base class methods", Justification = "Hidden base class methods eventually get called regardless.")] +internal sealed class ExtendedSignatureComparer : SignatureComparer, + IEqualityComparer, + IEqualityComparer, + IEqualityComparer +{ + public ExtendedSignatureComparer() + { + } + + public ExtendedSignatureComparer(SignatureComparisonFlags flags) : base(flags) + { + } + + public static new ExtendedSignatureComparer Default { get; } = new(); + public static ExtendedSignatureComparer VersionAgnostic { get; } = new(SignatureComparisonFlags.VersionAgnostic); + + + public bool Equals(IMemberDescriptor? x, IMemberDescriptor? y) + { + if (ReferenceEquals(x, y)) return true; + if (x == null || y == null) return false; + + return x switch + { + ITypeDescriptor type => base.Equals(type, y as ITypeDescriptor), + IMethodDescriptor method => base.Equals(method, y as IMethodDescriptor), + IFieldDescriptor field => base.Equals(field, y as IFieldDescriptor), + PropertyDefinition property => Equals(property, y as PropertyDefinition), + EventDefinition @event => Equals(@event, y as EventDefinition), + _ => false, + }; + } + + public int GetHashCode(IMemberDescriptor obj) + { + return obj switch + { + ITypeDescriptor type => base.GetHashCode(type), + IMethodDescriptor method => base.GetHashCode(method), + IFieldDescriptor field => base.GetHashCode(field), + PropertyDefinition property => GetHashCode(property), + EventDefinition @event => GetHashCode(@event), + _ => throw new ArgumentOutOfRangeException(nameof(obj)), + }; + } + + public bool Equals(PropertyDefinition? x, PropertyDefinition? y) + { + if (ReferenceEquals(x, y)) return true; + if (x == null || y == null) return false; + + return x.Name == y.Name && Equals(x.DeclaringType, y.DeclaringType); + } + + public int GetHashCode(PropertyDefinition obj) + { + return HashCode.Combine( + obj.Name, + obj.DeclaringType == null ? 0 : base.GetHashCode(obj.DeclaringType), + obj.Signature == null ? 0 : base.GetHashCode(obj.Signature) + ); + } + + public bool Equals(EventDefinition? x, EventDefinition? y) + { + if (ReferenceEquals(x, y)) return true; + if (x == null || y == null) return false; + + return x.Name == y.Name && base.Equals(x.DeclaringType, y.DeclaringType); + } + + public int GetHashCode(EventDefinition obj) + { + return HashCode.Combine( + obj.Name, + obj.DeclaringType == null ? 0 : base.GetHashCode(obj.DeclaringType), + obj.EventType == null ? 0 : base.GetHashCode(obj.EventType) + ); + } + + protected override bool SimpleTypeEquals(ITypeDescriptor x, ITypeDescriptor y) + { + // Check the basic properties first. + if (!x.IsTypeOf(y.Namespace, y.Name)) + return false; + + // If scope matches, it is a perfect match. + if (Equals(x.Scope, y.Scope)) + return true; + + // It can still be an exported type, we need to resolve the type then and check if the definitions match. + // For our purposes, we only actually care that the name matches + return x.Resolve() is { } definition1 + && y.Resolve() is { } definition2 + && Equals(definition1.DeclaringType, definition2.DeclaringType); + } + + protected override int SimpleTypeHashCode(ITypeDescriptor obj) + { + return HashCode.Combine( + obj.Namespace, + obj.Name, + obj.DeclaringType is not null ? GetHashCode(obj.DeclaringType) : 0 + ); + } + +}