diff --git a/MonoMod.Backports.slnx b/MonoMod.Backports.slnx index 3841476..ca155d6 100644 --- a/MonoMod.Backports.slnx +++ b/MonoMod.Backports.slnx @@ -18,8 +18,10 @@ - - - + + + + + diff --git a/src/MonoMod.Backports.Shims/ApiCompat.targets b/src/MonoMod.Backports.Shims/ApiCompat.targets index 294c2e7..5fd044d 100644 --- a/src/MonoMod.Backports.Shims/ApiCompat.targets +++ b/src/MonoMod.Backports.Shims/ApiCompat.targets @@ -1,210 +1,273 @@ - - - - - - - - - - <_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 + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + {{PKGREF}} + + + + <_DummyPackageReferenceTemplate> + + + + <_ApiCompatAsmOut>$(IntermediateOutputPath)apicompat/ + <_BackportsTfmsTxt2>$(IntermediateOutputPath)backports_tfms_final.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)" /> + + + + + + + + + + <_BackportsProps Remove="@(_BackportsProps)"/> + + + + + + + + + <_BackportsTfms>@(_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') + + + + + + + + + + + + + + + + + + + + <_GenApiCompatExe>%(MMGenApiCompat.RelativeDir)%(FileName)$(_NativeExecutableExtension) + + + + <_PPArguments Remove="@(_PPArguments)" /> + + <_PPArguments Include="$(IntermediateOutputPath)apicompat/" /> + + <_PPArguments Include="$(_BackportsTfmsTxt2)" /> + + <_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]) + + + + + + + + <_ArCompareDef Remove="@(_ArCompareDef)" /> + + + + + + <_Tfm>%(_GenApiCompatParsed.Tfm) + <_Dll>%(_GenApiCompatParsed.Dll) + <_LeftRefPath>%(_GenApiCompatParsed.LeftRefPath) + <_RightRefPath>%(_GenApiCompatParsed.RightRefPath) + <_RefPath> + <_RefPath Condition="'$(_Tfm)' == '%(_ApiCompatRefPath.Identity)'">%(_ApiCompatRefPath.ReferencePath) + + + + <_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)"/> + + + + + $(IntermediateOutputPath)comparisons.txt + + + + + + + + <_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 new file mode 100644 index 0000000..b074b51 --- /dev/null +++ b/src/MonoMod.Backports.Shims/CompatibilitySuppressions.xml @@ -0,0 +1,83 @@ + + + + + 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/MonoMod.Backports.Shims/Directory.Build.targets b/src/MonoMod.Backports.Shims/Directory.Build.targets index 11e3073..73fdf13 100644 --- a/src/MonoMod.Backports.Shims/Directory.Build.targets +++ b/src/MonoMod.Backports.Shims/Directory.Build.targets @@ -1,192 +1,270 @@ - - - - - - - - - - - - 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 + <_BackportsTfmsTxt>$(IntermediateOutputPath)backports_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)')" /> + + + + + + <_BackportsProps Remove="@(_BackportsProps)"/> + + + + + + + + + <_BackportsTfms>@(_BackportsProps->'%(Value)') + + + <_BackportsTfms1 Remove="@(_BackportsTfms1)" /> + <_BackportsTfms1 Include="$(_BackportsTfms)" /> + <_BackportsTfms1 Include="$(PackageMinTfms)" /> + <_BackportsTfms Remove="@(_BackportsTfms)" /> + <_BackportsTfms Include="@(_BackportsTfms1->Distinct())" /> + + + <_BackportsTfms>@(_BackportsTfms) + + + + + + + + + <_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="$(_BackportsTfmsTxt)" /> + + <_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)|9999.9999.9999.9999') + <_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..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 @@ -26,6 +26,11 @@ + + + + + @@ -36,5 +41,4 @@ - 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..ce719ec 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/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/build/ArApiCompat/ApiCompatibility/AssemblyMapping/AssemblyMapper.cs b/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/AssemblyMapper.cs new file mode 100644 index 0000000..a711143 --- /dev/null +++ b/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/AssemblyMapper.cs @@ -0,0 +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 (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/AssemblyMapping/ElementMapper.cs b/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/ElementMapper.cs new file mode 100644 index 0000000..ab0e426 --- /dev/null +++ b/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/ElementMapper.cs @@ -0,0 +1,42 @@ +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; + } +} diff --git a/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/ElementSide.cs b/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/ElementSide.cs new file mode 100644 index 0000000..6021620 --- /dev/null +++ b/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/ElementSide.cs @@ -0,0 +1,7 @@ +namespace ArApiCompat.ApiCompatibility.AssemblyMapping; + +public enum ElementSide : byte +{ + Left = 0, + Right = 1, +} diff --git a/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/MapperSettings.cs b/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/MapperSettings.cs new file mode 100644 index 0000000..bcd305a --- /dev/null +++ b/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/MapperSettings.cs @@ -0,0 +1,14 @@ +using ArApiCompat.Utilities.AsmResolver; +using AsmResolver.DotNet; + +namespace ArApiCompat.ApiCompatibility.AssemblyMapping; + +public sealed class MapperSettings +{ + public Func Filter { get; set; } = DefaultFilter; + + private static bool DefaultFilter(IMemberDefinition member) + { + return member.IsVisibleOutsideOfAssembly(); + } +} diff --git a/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/MemberMapper.cs b/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/MemberMapper.cs new file mode 100644 index 0000000..795cbc0 --- /dev/null +++ b/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/MemberMapper.cs @@ -0,0 +1,8 @@ +using AsmResolver.DotNet; + +namespace ArApiCompat.ApiCompatibility.AssemblyMapping; + +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 new file mode 100644 index 0000000..cc90364 --- /dev/null +++ b/src/build/ArApiCompat/ApiCompatibility/AssemblyMapping/TypeMapper.cs @@ -0,0 +1,59 @@ +using ArApiCompat.Utilities.AsmResolver; +using AsmResolver.DotNet; +using CompatUnbreaker.Tool.Utilities.AsmResolver; + +namespace ArApiCompat.ApiCompatibility.AssemblyMapping; + +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); + + 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 (MapperSettings.Filter(member)) + { + AddOrCreateMapper(member, side); + } + } + + foreach (var nestedType in value.NestedTypes) + { + if (MapperSettings.Filter(nestedType)) + { + AddOrCreateMapper(nestedType, side); + } + } + } + + private void AddOrCreateMapper(TypeDefinition nestedType, ElementSide side) + { + if (!_nestedTypes.TryGetValue(nestedType, out var mapper)) + { + mapper = new TypeMapper(MapperSettings, 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(MapperSettings, this); + _members.Add(member, mapper); + } + + mapper.Add(member, side); + } +} diff --git a/src/build/ArApiCompat/ApiCompatibility/Comparing/ApiComparer.cs b/src/build/ArApiCompat/ApiCompatibility/Comparing/ApiComparer.cs new file mode 100644 index 0000000..f7afc68 --- /dev/null +++ b/src/build/ArApiCompat/ApiCompatibility/Comparing/ApiComparer.cs @@ -0,0 +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 IReadOnlyList 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/CompatDifference.cs b/src/build/ArApiCompat/ApiCompatibility/Comparing/CompatDifference.cs new file mode 100644 index 0000000..cf17b97 --- /dev/null +++ b/src/build/ArApiCompat/ApiCompatibility/Comparing/CompatDifference.cs @@ -0,0 +1,32 @@ +using ArApiCompat.ApiCompatibility.AssemblyMapping; +using AsmResolver.DotNet; + +namespace ArApiCompat.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/src/build/ArApiCompat/ApiCompatibility/Comparing/DifferenceType.cs b/src/build/ArApiCompat/ApiCompatibility/Comparing/DifferenceType.cs new file mode 100644 index 0000000..58d3a78 --- /dev/null +++ b/src/build/ArApiCompat/ApiCompatibility/Comparing/DifferenceType.cs @@ -0,0 +1,8 @@ +namespace ArApiCompat.ApiCompatibility.Comparing; + +public enum DifferenceType +{ + Changed, + Added, + Removed, +} diff --git a/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/BaseRule.cs b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/BaseRule.cs new file mode 100644 index 0000000..b6c183e --- /dev/null +++ b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/BaseRule.cs @@ -0,0 +1,18 @@ +using ArApiCompat.ApiCompatibility.AssemblyMapping; + +namespace ArApiCompat.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/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotAddAbstractMember.cs b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotAddAbstractMember.cs new file mode 100644 index 0000000..142ebb6 --- /dev/null +++ b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotAddAbstractMember.cs @@ -0,0 +1,30 @@ +using ArApiCompat.ApiCompatibility.AssemblyMapping; +using ArApiCompat.Utilities.AsmResolver; + +namespace ArApiCompat.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 is not null && !leftDeclaringType.IsInterface && !leftDeclaringType.IsEffectivelySealed(/* TODO includeInternalSymbols */ false)) + { + differences.Add(new CannotAddAbstractMemberDifference(mapper)); + } + } + } +} diff --git a/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotAddMemberToInterface.cs b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotAddMemberToInterface.cs new file mode 100644 index 0000000..9f7bcd3 --- /dev/null +++ b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotAddMemberToInterface.cs @@ -0,0 +1,56 @@ +using ArApiCompat.ApiCompatibility.AssemblyMapping; +using AsmResolver.DotNet; +using CompatUnbreaker.Tool.Utilities.AsmResolver; + +namespace ArApiCompat.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 is { DeclaringType: not 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) + { + if (symbol.DeclaringType is null) return false; + + 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/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotAddOrRemoveVirtualKeyword.cs b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotAddOrRemoveVirtualKeyword.cs new file mode 100644 index 0000000..5437713 --- /dev/null +++ b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotAddOrRemoveVirtualKeyword.cs @@ -0,0 +1,60 @@ +using ArApiCompat.ApiCompatibility.AssemblyMapping; +using ArApiCompat.Utilities.AsmResolver; +using AsmResolver.DotNet; + +namespace ArApiCompat.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 not { DeclaringType: not null } || right is not { DeclaringType: not 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/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotChangeGenericConstraints.cs b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotChangeGenericConstraints.cs new file mode 100644 index 0000000..85c84b4 --- /dev/null +++ b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotChangeGenericConstraints.cs @@ -0,0 +1,167 @@ +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; + +#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 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 +{ + 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.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 + + 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); + } + } + + // 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))); + } + } + } + + private static HashSet ToHashSet(IList constraints) + { + return constraints + .Select(c => c.Constraint) + .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/Comparing/Rules/CannotChangeVisibility.cs b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotChangeVisibility.cs new file mode 100644 index 0000000..b4d31d4 --- /dev/null +++ b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotChangeVisibility.cs @@ -0,0 +1,77 @@ +using ArApiCompat.ApiCompatibility.AssemblyMapping; +using ArApiCompat.Utilities.AsmResolver; +using AsmResolver.DotNet; +using Accessibility = ArApiCompat.Utilities.AsmResolver.Accessibility; + +namespace ArApiCompat.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 static 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 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) + { + return; + } + + var leftAccess = left.GetAccessibility(); + var rightAccess = right.GetAccessibility(); + var accessComparison = CompareAccessibility(leftAccess, rightAccess); + + if (accessComparison > 0) + { + differences.Add(new CannotReduceVisibilityDifference(left, right)); + } + } +} diff --git a/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotRemoveBaseTypeOrInterface.cs b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotRemoveBaseTypeOrInterface.cs new file mode 100644 index 0000000..e46fe5e --- /dev/null +++ b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotRemoveBaseTypeOrInterface.cs @@ -0,0 +1,92 @@ +using ArApiCompat.ApiCompatibility.AssemblyMapping; +using ArApiCompat.Utilities.AsmResolver; +using AsmResolver.DotNet; +using CompatUnbreaker.Tool.Utilities.AsmResolver; + +namespace ArApiCompat.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 static 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 static 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/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotSealType.cs b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotSealType.cs new file mode 100644 index 0000000..ac64e62 --- /dev/null +++ b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/CannotSealType.cs @@ -0,0 +1,32 @@ +using ArApiCompat.ApiCompatibility.AssemblyMapping; +using ArApiCompat.Utilities.AsmResolver; + +namespace ArApiCompat.ApiCompatibility.Comparing.Rules; + +public sealed class CannotSealTypeDifference(TypeMapper mapper) : TypeCompatDifference(mapper) +{ + 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"}"; + + 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/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/EnumsMustMatch.cs b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/EnumsMustMatch.cs new file mode 100644 index 0000000..12993bd --- /dev/null +++ b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/EnumsMustMatch.cs @@ -0,0 +1,76 @@ +using ArApiCompat.ApiCompatibility.AssemblyMapping; +using ArApiCompat.Utilities.AsmResolver; +using AsmResolver.DotNet; + +namespace ArApiCompat.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/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/MembersMustExist.cs b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/MembersMustExist.cs new file mode 100644 index 0000000..8ca2b8e --- /dev/null +++ b/src/build/ArApiCompat/ApiCompatibility/Comparing/Rules/MembersMustExist.cs @@ -0,0 +1,92 @@ +using ArApiCompat.ApiCompatibility.AssemblyMapping; +using ArApiCompat.Utilities.AsmResolver; +using AsmResolver.DotNet; +using CompatUnbreaker.Tool.Utilities.AsmResolver; + +namespace ArApiCompat.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/src/build/ArApiCompat/ApiCompatibility/README.md b/src/build/ArApiCompat/ApiCompatibility/README.md new file mode 100644 index 0000000..f8e62d0 --- /dev/null +++ b/src/build/ArApiCompat/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/src/build/ArApiCompat/ApiCompatibility/Suppressions/SuppressionFile.cs b/src/build/ArApiCompat/ApiCompatibility/Suppressions/SuppressionFile.cs new file mode 100644 index 0000000..007b8d9 --- /dev/null +++ b/src/build/ArApiCompat/ApiCompatibility/Suppressions/SuppressionFile.cs @@ -0,0 +1,200 @@ +using ArApiCompat.ApiCompatibility.Comparing; +using System.Diagnostics.CodeAnalysis; +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 Suppression? GetSuppressionFor(Suppression suppression) + => Suppressions.FirstOrDefault(s => s == suppression); + } + + 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 new file mode 100644 index 0000000..b38574a --- /dev/null +++ b/src/build/ArApiCompat/ArApiCompat.csproj @@ -0,0 +1,16 @@ + + + + Exe + net9.0 + enable + false + enable + $(NoWarn);CA1043;CA1028;CA1031 + + + + + + + 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 new file mode 100644 index 0000000..f68a79f --- /dev/null +++ b/src/build/ArApiCompat/Program.cs @@ -0,0 +1,178 @@ +using ArApiCompat; +using ArApiCompat.ApiCompatibility.Suppressions; +using System.Xml.Linq; + +Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.InvariantCulture; +Thread.CurrentThread.CurrentUICulture = System.Globalization.CultureInfo.InvariantCulture; + +if (args is not [{ } suppressionFile, { } comparisonsDef, ..var rest]) +{ + 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); + +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 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, suppressionFileModel); + +if (writeSuppression) +{ + var suppressions = result.GetSuppressionFile(); + var doc = suppressions.Serialize(); + doc.Save(suppressionFile, SaveOptions.OmitDuplicateNamespaces); +} +else +{ + for (var i = 0; i < result.JobCount; i++) + { + var job = result.Jobs[i]; + var differences = result.GetDifferences(i); + if (differences.Count > 0) + { + anyError = true; + Console.WriteLine($"error : Compatability errors between '{job.LeftName}' and '{job.RightName}':"); + + foreach (var difference in differences) + { + Console.WriteLine($"error {difference}"); + } + + Console.WriteLine("---"); + } + else + { + // no differences, don't report anything + } + } + + if (result.HasUnusedSuppressions) + { + Console.WriteLine($"warning : Suppressions file '{suppressionFile}' has unused suppressions. {genSuppressionsMessage}"); + } +} + + +return anyError ? 1 : 0; \ No newline at end of file diff --git a/src/build/ArApiCompat/Utilities/AsmResolver/Accessibility.cs b/src/build/ArApiCompat/Utilities/AsmResolver/Accessibility.cs new file mode 100644 index 0000000..b76205c --- /dev/null +++ b/src/build/ArApiCompat/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 ArApiCompat.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/src/build/ArApiCompat/Utilities/AsmResolver/AccessibilityExtensions.cs b/src/build/ArApiCompat/Utilities/AsmResolver/AccessibilityExtensions.cs new file mode 100644 index 0000000..d1b7338 --- /dev/null +++ b/src/build/ArApiCompat/Utilities/AsmResolver/AccessibilityExtensions.cs @@ -0,0 +1,107 @@ +using AsmResolver.DotNet; +using AsmResolver.PE.DotNet.Metadata.Tables; + +namespace ArApiCompat.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 ArgumentOutOfRangeException(nameof(type)), + }; + } + + 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 ArgumentOutOfRangeException(nameof(method)), + }; + } + + 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 ArgumentOutOfRangeException(nameof(field)), + }; + } + + 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/src/build/ArApiCompat/Utilities/AsmResolver/DefinitionModifiersExtensions.cs b/src/build/ArApiCompat/Utilities/AsmResolver/DefinitionModifiersExtensions.cs new file mode 100644 index 0000000..86c4d81 --- /dev/null +++ b/src/build/ArApiCompat/Utilities/AsmResolver/DefinitionModifiersExtensions.cs @@ -0,0 +1,50 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.RegularExpressions; +using ArApiCompat.Utilities.AsmResolver; +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/src/build/ArApiCompat/Utilities/AsmResolver/ExtendedSignatureComparer.cs b/src/build/ArApiCompat/Utilities/AsmResolver/ExtendedSignatureComparer.cs new file mode 100644 index 0000000..aad62d9 --- /dev/null +++ b/src/build/ArApiCompat/Utilities/AsmResolver/ExtendedSignatureComparer.cs @@ -0,0 +1,119 @@ +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 + ); + } + +} diff --git a/src/build/ArApiCompat/Utilities/AsmResolver/GenericParameterExtensions.cs b/src/build/ArApiCompat/Utilities/AsmResolver/GenericParameterExtensions.cs new file mode 100644 index 0000000..236a82a --- /dev/null +++ b/src/build/ArApiCompat/Utilities/AsmResolver/GenericParameterExtensions.cs @@ -0,0 +1,13 @@ +using AsmResolver.DotNet; + +namespace ArApiCompat.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/src/build/ArApiCompat/Utilities/AsmResolver/MemberDefinitionExtensions.cs b/src/build/ArApiCompat/Utilities/AsmResolver/MemberDefinitionExtensions.cs new file mode 100644 index 0000000..9c50b8f --- /dev/null +++ b/src/build/ArApiCompat/Utilities/AsmResolver/MemberDefinitionExtensions.cs @@ -0,0 +1,100 @@ +using AsmResolver.DotNet; + +namespace ArApiCompat.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 + { + 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/src/build/ArApiCompat/Utilities/AsmResolver/MethodDefinitionExtensions.cs b/src/build/ArApiCompat/Utilities/AsmResolver/MethodDefinitionExtensions.cs new file mode 100644 index 0000000..f436834 --- /dev/null +++ b/src/build/ArApiCompat/Utilities/AsmResolver/MethodDefinitionExtensions.cs @@ -0,0 +1,111 @@ +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using AsmResolver.PE.DotNet.Metadata.Tables; + +namespace ArApiCompat.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/src/build/ArApiCompat/Utilities/AsmResolver/MiscellaneousExtensions.cs b/src/build/ArApiCompat/Utilities/AsmResolver/MiscellaneousExtensions.cs new file mode 100644 index 0000000..0e0dfdd --- /dev/null +++ b/src/build/ArApiCompat/Utilities/AsmResolver/MiscellaneousExtensions.cs @@ -0,0 +1,33 @@ +using AsmResolver.PE.DotNet.Cil; + +namespace ArApiCompat.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/src/build/ArApiCompat/Utilities/AsmResolver/TypeDefinitionExtensions.InterfaceMapping.cs b/src/build/ArApiCompat/Utilities/AsmResolver/TypeDefinitionExtensions.InterfaceMapping.cs new file mode 100644 index 0000000..ea3478d --- /dev/null +++ b/src/build/ArApiCompat/Utilities/AsmResolver/TypeDefinitionExtensions.InterfaceMapping.cs @@ -0,0 +1,43 @@ +using ArApiCompat.Utilities.AsmResolver; +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/src/build/ArApiCompat/Utilities/AsmResolver/TypeDefinitionExtensions.cs b/src/build/ArApiCompat/Utilities/AsmResolver/TypeDefinitionExtensions.cs new file mode 100644 index 0000000..f5ef4ee --- /dev/null +++ b/src/build/ArApiCompat/Utilities/AsmResolver/TypeDefinitionExtensions.cs @@ -0,0 +1,88 @@ +using ArApiCompat.Utilities; +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/src/build/ArApiCompat/Utilities/AsmResolver/TypeSignatureExtensions.cs b/src/build/ArApiCompat/Utilities/AsmResolver/TypeSignatureExtensions.cs new file mode 100644 index 0000000..41ee310 --- /dev/null +++ b/src/build/ArApiCompat/Utilities/AsmResolver/TypeSignatureExtensions.cs @@ -0,0 +1,32 @@ +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; + +namespace ArApiCompat.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/src/build/ArApiCompat/Utilities/AsmResolver/VisibilityExtensions.cs b/src/build/ArApiCompat/Utilities/AsmResolver/VisibilityExtensions.cs new file mode 100644 index 0000000..3367382 --- /dev/null +++ b/src/build/ArApiCompat/Utilities/AsmResolver/VisibilityExtensions.cs @@ -0,0 +1,43 @@ +using AsmResolver.DotNet; + +namespace ArApiCompat.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/src/build/ArApiCompat/Utilities/MetadataHelpers.cs b/src/build/ArApiCompat/Utilities/MetadataHelpers.cs new file mode 100644 index 0000000..4706efe --- /dev/null +++ b/src/build/ArApiCompat/Utilities/MetadataHelpers.cs @@ -0,0 +1,96 @@ +using System.Diagnostics; + +namespace ArApiCompat.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/src/build/ArApiCompat/Utilities/StringExtensions.cs b/src/build/ArApiCompat/Utilities/StringExtensions.cs new file mode 100644 index 0000000..d604590 --- /dev/null +++ b/src/build/ArApiCompat/Utilities/StringExtensions.cs @@ -0,0 +1,14 @@ +namespace ArApiCompat.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/src/build/FilterPackagesForRestore/FilterPackagesForRestore.csproj b/src/build/FilterPackagesForRestore/FilterPackagesForRestore.csproj new file mode 100644 index 0000000..248f4ac --- /dev/null +++ b/src/build/FilterPackagesForRestore/FilterPackagesForRestore.csproj @@ -0,0 +1,16 @@ + + + + Exe + net9.0 + enable + enable + false + + + + + + + + \ No newline at end of file diff --git a/src/build/FilterPackagesForRestore/Program.cs b/src/build/FilterPackagesForRestore/Program.cs new file mode 100644 index 0000000..53ef061 --- /dev/null +++ b/src/build/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/GenApiCompatDll.csproj b/src/build/GenApiCompatDll/GenApiCompatDll.csproj similarity index 84% rename from src/GenApiCompatDll/GenApiCompatDll.csproj rename to src/build/GenApiCompatDll/GenApiCompatDll.csproj index 7823bc7..0bf0557 100644 --- a/src/GenApiCompatDll/GenApiCompatDll.csproj +++ b/src/build/GenApiCompatDll/GenApiCompatDll.csproj @@ -11,6 +11,7 @@ + diff --git a/src/GenApiCompatDll/Program.cs b/src/build/GenApiCompatDll/Program.cs similarity index 76% rename from src/GenApiCompatDll/Program.cs rename to src/build/GenApiCompatDll/Program.cs index ed8eb6a..2d26808 100644 --- a/src/GenApiCompatDll/Program.cs +++ b/src/build/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 }) @@ -69,16 +118,32 @@ .. var dotnetOobPackagePaths // load our tfm list -var packageTfms = reducer.ReduceEquivalent( +var packageTfmsRaw = reducer.ReduceEquivalent( packages - .SelectMany(t => t.Value.fwks) + .SelectMany(t => t.Value) .Select(t => t.Key) ).ToArray(); -var tfms = reducer.ReduceEquivalent( + +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)); + +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(); @@ -89,7 +154,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 +298,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 +354,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 +363,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(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.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 89% rename from src/ShimGen/Program.cs rename to src/build/ShimGen/Program.cs index 4aeb5f8..bf99141 100644 --- a/src/ShimGen/Program.cs +++ b/src/build/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) { @@ -61,7 +62,7 @@ .. 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 @@ -90,15 +91,34 @@ .. 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 + +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) @@ -127,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 @@ -153,7 +173,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 +199,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); 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