Skip to content

Commit 35c5339

Browse files
authored
Update analyzer redirecting VS extension (#50496)
1 parent b62cacf commit 35c5339

File tree

9 files changed

+234
-56
lines changed

9 files changed

+234
-56
lines changed

documentation/general/analyzer-redirecting.md

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,21 +28,36 @@ Targeting an SDK (and hence also loading analyzers) with newer major version in
2828

2929
- Note that when `IAnalyzerAssemblyRedirector` is involved, Roslyn is free to not use shadow copy loading and instead load the DLLs directly.
3030

31+
- It is possible to opt out of analyzer redirecting by setting environment variable `DOTNET_ANALYZER_REDIRECTING=0`.
32+
That is an unsupported scenario though and compiler version mismatch errors will likely occur.
33+
3134
## Details
3235

3336
The VSIX contains some analyzers, for example:
3437

3538
```
36-
AspNetCoreAnalyzers\9.0.0-preview.5.24306.11\analyzers\dotnet\cs\Microsoft.AspNetCore.App.Analyzers.dll
37-
NetCoreAnalyzers\9.0.0-preview.5.24306.7\analyzers\dotnet\cs\System.Text.RegularExpressions.Generator.dll
38-
WindowsDesktopAnalyzers\9.0.0-preview.5.24306.8\analyzers\dotnet\System.Windows.Forms.Analyzers.dll
39-
SDKAnalyzers\9.0.100-dev\Sdks\Microsoft.NET.Sdk\analyzers\Microsoft.CodeAnalysis.NetAnalyzers.dll
40-
WebSDKAnalyzers\9.0.100-dev\Sdks\Microsoft.NET.Sdk.Web\analyzers\cs\Microsoft.AspNetCore.Analyzers.dll
39+
AspNetCoreAnalyzers\analyzers\dotnet\cs\Microsoft.AspNetCore.App.Analyzers.dll
40+
NetCoreAnalyzers\analyzers\dotnet\cs\System.Text.RegularExpressions.Generator.dll
41+
WindowsDesktopAnalyzers\analyzers\dotnet\System.Windows.Forms.Analyzers.dll
42+
SDKAnalyzers\Sdks\Microsoft.NET.Sdk\analyzers\Microsoft.CodeAnalysis.NetAnalyzers.dll
43+
WebSDKAnalyzers\Sdks\Microsoft.NET.Sdk.Web\analyzers\cs\Microsoft.AspNetCore.Analyzers.dll
44+
```
45+
46+
And metadata at `metadata.json`:
47+
48+
```json
49+
{
50+
"AspNetCoreAnalyzers": "9.0.0-preview.5.24306.11",
51+
"NetCoreAnalyzers": "9.0.0-preview.5.24306.7",
52+
"WindowsDesktopAnalyzers": "9.0.0-preview.5.24306.8",
53+
"SDKAnalyzers": "9.0.100-dev",
54+
"WebSDKAnalyzers": "9.0.100-dev",
55+
}
4156
```
4257

4358
Given an analyzer assembly load going through our `IAnalyzerAssemblyRedirector`,
4459
we will redirect it if the original path of the assembly being loaded matches the path of a VSIX-deployed analyzer -
45-
only segments of these paths starting after the version segment are compared,
60+
only relevant segments (see example below) of these paths are compared,
4661
plus the major and minor component of the versions must match.
4762

4863
For example, the analyzer
@@ -54,15 +69,20 @@ C:\Program Files\dotnet\sdk\9.0.100-preview.5.24307.3\Sdks\Microsoft.NET.Sdk\ana
5469
will be redirected to
5570

5671
```
57-
{VSIX}\SDKAnalyzers\9.0.100-dev\Sdks\Microsoft.NET.Sdk\analyzers\Microsoft.CodeAnalysis.NetAnalyzers.dll
72+
{VSIX}\SDKAnalyzers\Sdks\Microsoft.NET.Sdk\analyzers\Microsoft.CodeAnalysis.NetAnalyzers.dll
5873
```
5974

60-
because
75+
where `metadata.json` has `"SDKAnalyzers": "9.0.100-dev"`, because
6176
1. the suffix `Sdks\Microsoft.NET.Sdk\analyzers\Microsoft.CodeAnalysis.NetAnalyzers.dll` matches, and
6277
2. the version `9.0.100-preview.5.24307.3` has the same major and minor component (`9.0`) as the version `9.0.100-dev`
6378
(both versions are read from the paths, not DLL metadata).
6479

6580
Analyzers that cannot be matched will continue to be loaded from the SDK
6681
(and will fail to load if they reference Roslyn that is newer than is in VS).
6782

83+
### Implementation
84+
85+
Analyzer DLLs are contained in transport package `VS.Redist.Common.Net.Core.SDK.RuntimeAnalyzers`.
86+
The redirecting logic lives in "system" VS extension `Microsoft.Net.Sdk.AnalyzerRedirecting`.
87+
6888
[torn-sdk]: https://github.com/dotnet/sdk/issues/42087

src/Layout/VS.Redist.Common.Net.Core.SDK.RuntimeAnalyzers/VS.Redist.Common.Net.Core.SDK.RuntimeAnalyzers.proj

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.Build.NoTargets">
1+
<Project Sdk="Microsoft.Build.NoTargets">
22

33
<PropertyGroup>
44
<TargetFramework>net472</TargetFramework>
@@ -21,6 +21,7 @@
2121
<Target Name="GenerateLayout" Condition="'$(IsPackable)' == 'true'" DependsOnTargets="ResolveProjectReferences">
2222
<PropertyGroup>
2323
<SdkRuntimeAnalyzersSwrFile>$(ArtifactsNonShippingPackagesDir)VS.Redist.Common.Net.Core.SDK.RuntimeAnalyzers.swr</SdkRuntimeAnalyzersSwrFile>
24+
<SdkRuntimeAnalyzersMetadataFile>$(OutputPath)\metadata.json</SdkRuntimeAnalyzersMetadataFile>
2425
</PropertyGroup>
2526

2627
<ItemGroup>
@@ -35,21 +36,51 @@
3536
<Error Condition="'@(RedistRuntimeAnalyzersContent)' == ''" Text="The 'RedistRuntimeAnalyzersContent' items are empty. This shouldn't happen!" />
3637
<Error Condition="'@(RedirectingRuntimeAnalyzersContent)' == ''" Text="The 'RedirectingRuntimeAnalyzersContent' items are empty. This shouldn't happen!" />
3738

39+
<!-- The custom task cannot access %(RecursiveDir) without this. -->
3840
<ItemGroup>
39-
<RuntimeAnalyzersContent Include="@(RedistRuntimeAnalyzersContent);@(RedirectingRuntimeAnalyzersContent)" />
41+
<RedistRuntimeAnalyzersContent Update="@(RedistRuntimeAnalyzersContent)">
42+
<CustomRecursiveDir>%(RecursiveDir)</CustomRecursiveDir>
43+
</RedistRuntimeAnalyzersContent>
44+
<RedirectingRuntimeAnalyzersContent Update="@(RedirectingRuntimeAnalyzersContent)">
45+
<CustomRecursiveDir>%(RecursiveDir)</CustomRecursiveDir>
46+
</RedirectingRuntimeAnalyzersContent>
47+
</ItemGroup>
48+
49+
<ProcessRuntimeAnalyzerVersions Inputs="@(RedistRuntimeAnalyzersContent)" MetadataFilePath="$(SdkRuntimeAnalyzersMetadataFile)">
50+
<Output TaskParameter="Outputs" ItemName="ProcessedRedistRuntimeAnalyzersContent" />
51+
</ProcessRuntimeAnalyzerVersions>
52+
53+
<ItemGroup>
54+
<RuntimeAnalyzersContent Include="@(ProcessedRedistRuntimeAnalyzersContent);@(RedirectingRuntimeAnalyzersContent)" />
4055
</ItemGroup>
4156

4257
<Copy SourceFiles="@(RuntimeAnalyzersContent)"
43-
DestinationFiles="@(RuntimeAnalyzersContent->'$(OutputPath)\%(DeploymentSubpath)\%(RecursiveDir)%(Filename)%(Extension)')"
58+
DestinationFiles="@(RuntimeAnalyzersContent->'$(OutputPath)\%(DeploymentSubpath)\%(CustomRecursiveDir)%(Filename)%(Extension)')"
4459
UseHardlinksIfPossible="true" />
4560

61+
<!-- Replace Experimental=true with SystemComponent=true in the VS extension manifest. -->
62+
<PropertyGroup>
63+
<_VsixNamespace>
64+
<Namespace Prefix="pm" Uri="http://schemas.microsoft.com/developer/vsx-schema/2011" />
65+
</_VsixNamespace>
66+
</PropertyGroup>
67+
<XmlPoke XmlInputPath="$(OutputPath)\AnalyzerRedirecting\extension.vsixmanifest"
68+
Namespaces="$(_VsixNamespace)"
69+
Query="/pm:PackageManifest/pm:Installation/@Experimental"
70+
Value="false" />
71+
<XmlPoke XmlInputPath="$(OutputPath)\AnalyzerRedirecting\extension.vsixmanifest"
72+
Namespaces="$(_VsixNamespace)"
73+
Query="/pm:PackageManifest/pm:Installation/@SystemComponent"
74+
Value="true" />
75+
4676
<GenerateRuntimeAnalyzersSWR RuntimeAnalyzersLayoutDirectory="$(OutputPath)"
4777
OutputFile="$(SdkRuntimeAnalyzersSwrFile)" />
4878

4979
<ItemGroup>
5080
<!-- Include the swr file in the nuget package for VS authoring -->
5181
<Content Include="$(SdkRuntimeAnalyzersSwrFile)" PackagePath="/" />
52-
<Content Include="@(RuntimeAnalyzersContent)" PackagePath="/%(RuntimeAnalyzersContent.DeploymentSubpath)/%(RecursiveDir)%(Filename)%(Extension)" />
82+
<Content Include="$(SdkRuntimeAnalyzersMetadataFile)" PackagePath="/" />
83+
<Content Include="@(RuntimeAnalyzersContent)" PackagePath="/%(RuntimeAnalyzersContent.DeploymentSubpath)/%(RuntimeAnalyzersContent.CustomRecursiveDir)%(Filename)%(Extension)" />
5384
</ItemGroup>
5485
</Target>
5586

src/Microsoft.Net.Sdk.AnalyzerRedirecting/Microsoft.Net.Sdk.AnalyzerRedirecting.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" />
2525
<PackageReference Include="Microsoft.VisualStudio.Sdk" />
2626
<PackageReference Include="Microsoft.VSSDK.BuildTools" PrivateAssets="all" />
27+
<PackageReference Include="System.Text.Json" VersionOverride="9.0.0" />
2728
</ItemGroup>
2829

2930
<ItemGroup>

src/Microsoft.Net.Sdk.AnalyzerRedirecting/SdkAnalyzerAssemblyRedirector.cs

Lines changed: 77 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33

44
using System.Collections.Immutable;
55
using System.ComponentModel.Composition;
6+
using System.Text.Json;
67
using Microsoft.CodeAnalysis.Workspaces.AnalyzerRedirecting;
8+
using Microsoft.VisualStudio.Shell;
9+
using Microsoft.VisualStudio.Shell.Interop;
710

811
// Example:
912
// FullPath: "C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.8\analyzers\dotnet\System.Windows.Forms.Analyzers.dll"
@@ -13,9 +16,16 @@
1316

1417
namespace Microsoft.Net.Sdk.AnalyzerRedirecting;
1518

19+
/// <summary>
20+
/// See <c>documentation/general/analyzer-redirecting.md</c>.
21+
/// </summary>
1622
[Export(typeof(IAnalyzerAssemblyRedirector))]
1723
public sealed class SdkAnalyzerAssemblyRedirector : IAnalyzerAssemblyRedirector
1824
{
25+
private readonly IVsActivityLog? _log;
26+
27+
private readonly bool _enabled;
28+
1929
private readonly string? _insertedAnalyzersDirectory;
2030

2131
/// <summary>
@@ -24,62 +34,91 @@ public sealed class SdkAnalyzerAssemblyRedirector : IAnalyzerAssemblyRedirector
2434
private readonly ImmutableDictionary<string, List<AnalyzerInfo>> _analyzerMap;
2535

2636
[ImportingConstructor]
27-
public SdkAnalyzerAssemblyRedirector()
28-
: this(Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"..\..\DotNetRuntimeAnalyzers"))) { }
37+
public SdkAnalyzerAssemblyRedirector(SVsServiceProvider serviceProvider) : this(
38+
Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"CommonExtensions\Microsoft\DotNet")),
39+
serviceProvider.GetService<SVsActivityLog, IVsActivityLog>())
40+
{
41+
}
2942

3043
// Internal for testing.
31-
internal SdkAnalyzerAssemblyRedirector(string? insertedAnalyzersDirectory)
44+
internal SdkAnalyzerAssemblyRedirector(string? insertedAnalyzersDirectory, IVsActivityLog? log = null)
3245
{
46+
_log = log;
47+
var enable = Environment.GetEnvironmentVariable("DOTNET_ANALYZER_REDIRECTING");
48+
_enabled = !"0".Equals(enable, StringComparison.OrdinalIgnoreCase) && !"false".Equals(enable, StringComparison.OrdinalIgnoreCase);
3349
_insertedAnalyzersDirectory = insertedAnalyzersDirectory;
3450
_analyzerMap = CreateAnalyzerMap();
3551
}
3652

3753
private ImmutableDictionary<string, List<AnalyzerInfo>> CreateAnalyzerMap()
3854
{
55+
if (!_enabled)
56+
{
57+
Log("Analyzer redirecting is disabled.");
58+
return ImmutableDictionary<string, List<AnalyzerInfo>>.Empty;
59+
}
60+
61+
var metadataFilePath = Path.Combine(_insertedAnalyzersDirectory, "metadata.json");
62+
if (!File.Exists(metadataFilePath))
63+
{
64+
Log($"File does not exist: {metadataFilePath}");
65+
return ImmutableDictionary<string, List<AnalyzerInfo>>.Empty;
66+
}
67+
68+
var versions = JsonSerializer.Deserialize<Dictionary<string, string>>(File.ReadAllText(metadataFilePath));
69+
if (versions is null || versions.Count == 0)
70+
{
71+
Log($"Versions are empty: {metadataFilePath}");
72+
return ImmutableDictionary<string, List<AnalyzerInfo>>.Empty;
73+
}
74+
3975
var builder = ImmutableDictionary.CreateBuilder<string, List<AnalyzerInfo>>(StringComparer.OrdinalIgnoreCase);
4076

4177
// Expects layout like:
42-
// VsInstallDir\SDK\RuntimeAnalyzers\WindowsDesktopAnalyzers\8.0.8\analyzers\dotnet\System.Windows.Forms.Analyzers.dll
43-
// ~~~~~~~~~~~~~~~~~~~~~~~ = topLevelDirectory
44-
// ~~~~~ = versionDirectory
45-
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ = analyzerPath
78+
// VsInstallDir\DotNetRuntimeAnalyzers\WindowsDesktopAnalyzers\analyzers\dotnet\System.Windows.Forms.Analyzers.dll
79+
// ~~~~~~~~~~~~~~~~~~~~~~~ = topLevelDirectory
80+
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ = analyzerPath
4681

4782
foreach (string topLevelDirectory in Directory.EnumerateDirectories(_insertedAnalyzersDirectory))
4883
{
49-
foreach (string versionDirectory in Directory.EnumerateDirectories(topLevelDirectory))
84+
foreach (string analyzerPath in Directory.EnumerateFiles(topLevelDirectory, "*.dll", SearchOption.AllDirectories))
5085
{
51-
foreach (string analyzerPath in Directory.EnumerateFiles(versionDirectory, "*.dll", SearchOption.AllDirectories))
86+
if (!analyzerPath.StartsWith(topLevelDirectory, StringComparison.OrdinalIgnoreCase))
5287
{
53-
if (!analyzerPath.StartsWith(versionDirectory, StringComparison.OrdinalIgnoreCase))
54-
{
55-
continue;
56-
}
57-
58-
string version = Path.GetFileName(versionDirectory);
59-
string analyzerName = Path.GetFileNameWithoutExtension(analyzerPath);
60-
string pathSuffix = analyzerPath.Substring(versionDirectory.Length + 1 /* slash */);
61-
pathSuffix = Path.GetDirectoryName(pathSuffix);
62-
63-
AnalyzerInfo analyzer = new() { FullPath = analyzerPath, ProductVersion = version, PathSuffix = pathSuffix };
64-
65-
if (builder.TryGetValue(analyzerName, out var existing))
66-
{
67-
existing.Add(analyzer);
68-
}
69-
else
70-
{
71-
builder.Add(analyzerName, [analyzer]);
72-
}
88+
continue;
89+
}
90+
91+
string subsetName = Path.GetFileName(topLevelDirectory);
92+
if (!versions.TryGetValue(subsetName, out string version))
93+
{
94+
continue;
95+
}
96+
97+
string analyzerName = Path.GetFileNameWithoutExtension(analyzerPath);
98+
string pathSuffix = analyzerPath.Substring(topLevelDirectory.Length + 1 /* slash */);
99+
pathSuffix = Path.GetDirectoryName(pathSuffix);
100+
101+
AnalyzerInfo analyzer = new() { FullPath = analyzerPath, ProductVersion = version, PathSuffix = pathSuffix };
102+
103+
if (builder.TryGetValue(analyzerName, out var existing))
104+
{
105+
existing.Add(analyzer);
106+
}
107+
else
108+
{
109+
builder.Add(analyzerName, [analyzer]);
73110
}
74111
}
75112
}
76113

114+
Log($"Loaded analyzer map ({builder.Count}): {_insertedAnalyzersDirectory}");
115+
77116
return builder.ToImmutable();
78117
}
79118

80119
public string? RedirectPath(string fullPath)
81120
{
82-
if (_analyzerMap.TryGetValue(Path.GetFileNameWithoutExtension(fullPath), out var analyzers))
121+
if (_enabled && _analyzerMap.TryGetValue(Path.GetFileNameWithoutExtension(fullPath), out var analyzers))
83122
{
84123
foreach (AnalyzerInfo analyzer in analyzers)
85124
{
@@ -134,4 +173,12 @@ static bool areVersionMajorMinorPartEqual(string version1, string version2)
134173
return 0 == string.Compare(version1, 0, version2, 0, secondDotIndex, StringComparison.OrdinalIgnoreCase);
135174
}
136175
}
176+
177+
private void Log(string message)
178+
{
179+
_log?.LogEntry(
180+
(uint)__ACTIVITYLOG_ENTRYTYPE.ALE_INFORMATION,
181+
nameof(SdkAnalyzerAssemblyRedirector),
182+
message);
183+
}
137184
}

src/Microsoft.Net.Sdk.AnalyzerRedirecting/source.extension.vsixmanifest

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@
1313
<Asset Type="Microsoft.VisualStudio.VsPackage" Path="|%CurrentProject%;PkgdefProjectOutputGroup|" d:Source="Project" d:ProjectName="%CurrentProject%" />
1414
</Assets>
1515
<Prerequisites>
16-
<Prerequisite Id="Microsoft.VisualStudio.Component.CoreEditor" Version="[17.0,18.0)" DisplayName="Visual Studio core editor" />
16+
<Prerequisite Id="Microsoft.VisualStudio.Component.CoreEditor" Version="[17.0,19.0)" DisplayName="Visual Studio core editor" />
1717
</Prerequisites>
18-
<Installation Experimental="true">
19-
<InstallationTarget Id="Microsoft.VisualStudio.Pro" Version="[17.0,18.0)">
18+
<Installation Experimental="true" SystemComponent="false">
19+
<InstallationTarget Id="Microsoft.VisualStudio.Pro" Version="[17.0,19.0)">
2020
<ProductArchitecture>amd64</ProductArchitecture>
2121
</InstallationTarget>
2222
</Installation>

src/Tasks/sdk-tasks/GenerateRuntimeAnalyzersSWR.cs

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,19 @@ public override bool Execute()
1919

2020
// NOTE: Keep in sync with SdkAnalyzerAssemblyRedirector.
2121
// This is intentionally short to avoid long path problems.
22-
const string installDir = @"DotNetRuntimeAnalyzers";
22+
const string installDir = @"Common7\IDE\CommonExtensions\Microsoft\DotNet";
2323

2424
AddFolder(sb,
25-
@"AnalyzerRedirecting",
26-
@"Common7\IDE\CommonExtensions\Microsoft\AnalyzerRedirecting",
25+
"",
26+
installDir,
27+
filesToInclude:
28+
[
29+
"metadata.json",
30+
]);
31+
32+
AddFolder(sb,
33+
"AnalyzerRedirecting",
34+
@$"{installDir}\AnalyzerRedirecting",
2735
filesToInclude:
2836
[
2937
"Microsoft.Net.Sdk.AnalyzerRedirecting.dll",
@@ -32,23 +40,23 @@ public override bool Execute()
3240
]);
3341

3442
AddFolder(sb,
35-
@"AspNetCoreAnalyzers",
43+
"AspNetCoreAnalyzers",
3644
@$"{installDir}\AspNetCoreAnalyzers");
3745

3846
AddFolder(sb,
39-
@"NetCoreAnalyzers",
47+
"NetCoreAnalyzers",
4048
@$"{installDir}\NetCoreAnalyzers");
4149

4250
AddFolder(sb,
43-
@"WindowsDesktopAnalyzers",
51+
"WindowsDesktopAnalyzers",
4452
@$"{installDir}\WindowsDesktopAnalyzers");
4553

4654
AddFolder(sb,
47-
@"SDKAnalyzers",
55+
"SDKAnalyzers",
4856
@$"{installDir}\SDKAnalyzers");
4957

5058
AddFolder(sb,
51-
@"WebSDKAnalyzers",
59+
"WebSDKAnalyzers",
5260
@$"{installDir}\WebSDKAnalyzers");
5361

5462
File.WriteAllText(OutputFile, sb.ToString());

0 commit comments

Comments
 (0)