dotnet/roslyn-analyzers

Sharp edges when `CentralPackageTransitivePinningEnabled=true` in source generator projects

austindrenski opened this issue · 2 comments

This isn't necessarily a bug, but it had me stumped all morning, so wanted to flag it for future readers, and maybe for the Roslyn team to consider a diagnostic warning as part of EnforceExtendedAnalyzerRules.

Basically, turning on CentralPackageTransitivePinningEnabled caused our source generators to fail in Rider and Visual Studio (e.g. unable to find System.Collections.Immutable), but notably they did not fail when running dotnet build.

The source generators in question are bare bones and only depend on Microsoft.CodeAnalysis.CSharp so it took a while to realize that CentralPackageTransitivePinningEnabled was causing our source generator package to include a reference (but not as a dependency!) to the RC version of System.Collections.Immutable instead of the compiler-and-IDE-safe reference that usually flows from Microsoft.CodeAnalysis.CSharp.

So again, not a bug since this is exactly what CentralPackageTransitivePinningEnabled is supposed to do, but was a confusing triage given how source generator failures are displayed in the IDEs.

What might be nice is a meta-analyzer to warn you when a source generator project has CentralPackageTransitivePinningEnabled=true. In a perfect world, this meta-analyzer would only warn when CentralPackageTransitivePinningEnabled=true and one of the project's transitive dependencies was actually being pinned, but not sure if the analyzer would have sufficient context for that.

// global.json
{
  "sdk": {
    "version": "9.0.100-rc.2.24474.11"
  }
}
<!-- Directory.Packages.props -->
<Project>

  <PropertyGroup>
    <CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
  </PropertyGroup>

  <ItemGroup>
    <!-- included for source generator project -->
    <PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" />
    <!-- included for normal project -->
    <PackageVersion Include="System.Collections.Immutable" Version="9.0.0-rc.2.24473.5" />
  </ItemGroup>

</Project>
<!-- SomeSourceGeneratorProject.csproj -->
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
    <IncludeBuildOutput>false</IncludeBuildOutput>
    <IncludeSymbols>false</IncludeSymbols>
    <NoPackageAnalysis>true</NoPackageAnalysis>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <None Include="_._" Pack="true" PackagePath="lib/$(TargetFramework)" Visible="false" />
    <None Include="$(OutputPath)$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
  </ItemGroup>

</Project>

Image


edit: including our work-around below:

<!-- SomeSourceGeneratorProject.csproj -->
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
+   <CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
    <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
    <IncludeBuildOutput>false</IncludeBuildOutput>
    <IncludeSymbols>false</IncludeSymbols>
    <NoPackageAnalysis>true</NoPackageAnalysis>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <None Include="_._" Pack="true" PackagePath="lib/$(TargetFramework)" Visible="false" />
    <None Include="$(OutputPath)$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
  </ItemGroup>

</Project>

edit: including a (perhaps) better work-around based on the discussion below:

<!-- Directory.Packages.props -->
<Project>

  <PropertyGroup>
    <CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
  </PropertyGroup>

  <ItemGroup>
    <!-- included for source generator project -->
    <PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" />
    <!-- included for normal project -->
-    <PackageVersion Include="System.Collections.Immutable" Version="9.0.0-rc.2.24473.5" />
+    <PackageVersion Include="System.Collections.Immutable" Version="9.0.0-rc.2.24473.5" GeneratePackagePath="true" />
  </ItemGroup>

</Project>

<!-- SomeSourceGeneratorProject.csproj -->
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
    <IncludeBuildOutput>false</IncludeBuildOutput>
    <IncludeSymbols>false</IncludeSymbols>
    <NoPackageAnalysis>true</NoPackageAnalysis>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <None Include="_._" Pack="true" PackagePath="lib/$(TargetFramework)" Visible="false" />
    <None Include="$(OutputPath)$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
+   <None Include="$(PkgSystem_Collections_Immutable)/lib/$(TargetFramework)/*.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
  </ItemGroup>

</Project>

edit: including a target that can be dropped into the csproj and does this in a less icky way

<Target Name="_IncludeAnalyzerDependencies" AfterTargets="GetTargetPath">

  <ItemGroup>
    <_AnalyzerDependency Include="@(ReferenceCopyLocalPaths)" Condition="'%(Extension)' == '.dll'" />
    <_AnalyzerDependency Include="@(RuntimeCopyLocalItems)" Exclude="@(ReferencesFromSDK)" />
    <_AnalyzerDependency Include="@(TargetPathWithTargetPlatformMoniker)" />
  </ItemGroup>

  <ItemGroup>
    <None Include="@(_AnalyzerDependency)" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
  </ItemGroup>

</Target>
<!-- Directory.Packages.props -->
<Project>

  <PropertyGroup>
    <CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
  </PropertyGroup>

  <ItemGroup>
    <!-- included for source generator project -->
    <PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" />
    <!-- included for normal project -->
    <PackageVersion Include="System.Collections.Immutable" Version="9.0.0-rc.2.24473.5" />
  </ItemGroup>

</Project>

<!-- SomeSourceGeneratorProject.csproj -->
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
    <IncludeBuildOutput>false</IncludeBuildOutput>
    <IncludeSymbols>false</IncludeSymbols>
    <NoPackageAnalysis>true</NoPackageAnalysis>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <None Include="_._" Pack="true" PackagePath="lib/$(TargetFramework)" Visible="false" />
-    <None Include="$(OutputPath)$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
  </ItemGroup>
+ 
+  <Target Name="_IncludeAnalyzerDependencies" AfterTargets="GetTargetPath">
+  
+    <ItemGroup>
+      <_AnalyzerDependency Include="@(ReferenceCopyLocalPaths)" Condition="'%(Extension)' == '.dll'" />
+      <_AnalyzerDependency Include="@(RuntimeCopyLocalItems)" Exclude="@(ReferencesFromSDK)" />
+      <_AnalyzerDependency Include="@(TargetPathWithTargetPlatformMoniker)" />
+    </ItemGroup>
+  
+    <ItemGroup>
+      <None Include="@(_AnalyzerDependency)" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
+    </ItemGroup>
+  
+  </Target>

</Project>

edit: continuing to update this for future readers...

<!-- Directory.Build.targets -->
<Project>

  <PropertyGroup Condition="'$(IsAnalyzer)' == 'true'">
    <!-- TODO: https://github.com/dotnet/roslyn-analyzers/issues/7460 -->
    <CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
    <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
    <GetTargetPathDependsOn>$(GetTargetPathDependsOn);_IncludeAnalyzerDependencies</GetTargetPathDependsOn>
    <IncludeBuildOutput>false</IncludeBuildOutput>
    <IncludeSymbols>false</IncludeSymbols>
    <NoPackageAnalysis>true</NoPackageAnalysis>
  </PropertyGroup>

  <!--
    ============================================================
    _IncludeAnalyzerDependencies
    ============================================================
    Include analyzer runtime dependencies that are not provided
    or otherwise resolvable by Roslyn for design-time builds.
    ============================================================
  -->
  <Target Name="_IncludeAnalyzerDependencies" Returns="@(_AnalyzerDependency)">

    <ItemGroup>
      <_RoslynDependency Include="@(RuntimeCopyLocalItems)" Condition="$([System.Text.RegularExpressions.Regex]::IsMatch(%(Identity), '^.*[/\\]Microsoft\.CodeAnalysis(\.[^./\\]+)*.dll$'))" />
    </ItemGroup>

    <ItemGroup>
      <_AnalyzerDependency Include="@(RuntimeCopyLocalItems)" Exclude="@(_RoslynDependency)" />
      <_AnalyzerDependency Include="@(ReferencePath)" Condition="'%(ReferencePath.ReferenceSourceTarget)' == 'ProjectReference'" />
    </ItemGroup>

    <ItemGroup>
      <None Include="@(_AnalyzerDependency);$(TargetPath)" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
    </ItemGroup>

    <ItemGroup>
      <TargetPathWithTargetPlatformMoniker Include="@(_AnalyzerDependency)" IncludeRuntimeDependency="false" ResolveableAssembly="false" />
    </ItemGroup>

  </Target>

</Project>

What might be nice is a meta-analyzer to warn you when a source generator project has CentralPackageTransitivePinningEnabled=true. I

This is a very unlikely course for us to be taking. The guidance for .NET is pushing the ecosystem into using CPM and transitive pinning because it a much easier system for managing dependencies. There is nothing special about source generators that would lead us to giving different guidance here.

What might be nice is a meta-analyzer to warn you when a source generator project has CentralPackageTransitivePinningEnabled=true

The root problem here is that your analyzer / generator ended up taking a dependency on a version of System.Collections.Immutable that was higher than what the compiler used. I can see us wanting thinking about a warning for that as it does represent a likely issue in your build.

What might be nice is a meta-analyzer to warn you when a source generator project has CentralPackageTransitivePinningEnabled=true. I

This is a very unlikely course for us to be taking. The guidance for .NET is pushing the ecosystem into using CPM and transitive pinning because it a much easier system for managing dependencies. There is nothing special about source generators that would lead us to giving different guidance here.

Of course! Just to clarify, I didn't mean to suggest a warning about CPM/CPTPE, but more so that due to a CPTPE-induced dependency version change, an additional artifact needs to be packed into analyzers/dotnet/cs.

For the time-being, I've just slapped CentralPackageTransitivePinningEnabled=false into the affected projects (i.e. overriding the default from Directory.Packages.props), which does the job, but feels like a sledgehammer of a solution.

So the alternative is that I could pack the bumped dependencies into my NuGet package, but there's not much guidance on how that might affect things for consumers.

I haven't seen much about this in the analyzer/generator design docs, but I assume there's a reason they don't recommend shipping analyzer/generator packages fully self-contained with all in analyzers/dotnet/cs. For example, if all of the analyzers/generators I ship now pack their own copy of System.Collections.Immutable v9.0.0-rc.2, will devs consuming these feel the impact in their IDEs?¹

What might be nice is a meta-analyzer to warn you when a source generator project has CentralPackageTransitivePinningEnabled=true

The root problem here is that your analyzer / generator ended up taking a dependency on a version of System.Collections.Immutable that was higher than what the compiler used. I can see us wanting thinking about a warning for that as it does represent a likely issue in your build.

+1


¹(System.Collections.Immutable is perhaps a poor example here, since this particular issue goes away once .NET 9 GA ships next month. But all the same, would still be curious to hear your thoughts on this.)